ask-rag
RAG pipeline for the ask-rb ecosystem. Load documents, split them into chunks, store embeddings, and retrieve relevant context — all from Ruby.
gem "ask-rag"
Quick Start
require "ask-rag"
# 1. Load and split
loader = Ask::RAG::Loader::Text.new
docs = loader.load("README.md")
splitter = Ask::RAG::TextSplitter::RecursiveCharacter.new(
chunk_size: 500, chunk_overlap: 50
)
chunks = splitter.split_documents(docs)
# 2. Embed and store
store = Ask::RAG::VectorStore::InMemory.new
store.add(chunks, model: "text-embedding-3-small")
# 3. Search
results = store.similarity_search("How do I install ask-rag?", limit: 3)
results.each do |doc|
puts "[#{doc.[:score].round(3)}] #{doc.content[0..80]}..."
end
# 4. Or one-shot RAG query
answer = Ask::RAG::Query.query(
store: store,
question: "What are the dependencies?",
model: "gpt-4o"
)
puts answer.content
Components
Loaders
Load files into Ask::Document objects (text + metadata).
| Loader | Format | Dependency |
|---|---|---|
Ask::RAG::Loader::Text |
Plain text | None (stdlib) |
Ask::RAG::Loader::Markdown |
Markdown | None (stdlib) |
Ask::RAG::Loader::CSV |
CSV (1 doc/row) | None (csv gem) |
Ask::RAG::Loader::HTML |
HTML (strips script/style/nav) | nokogiri |
Ask::RAG::Loader::PDF |
PDF (1 doc/page) | pdf-reader |
Ask::RAG::Loader::Directory |
Auto-detects file types by extension | None (uses other loaders internally) |
Loaders without their dependency gem installed are skipped silently — install the gem when you need the format.
loader = Ask::RAG::Loader::PDF.new
docs = loader.load("manual.pdf")
docs.length # => number of pages
docs[0]. # => { source: "manual.pdf", page: 1, format: "pdf" }
Text Splitters
Split documents into smaller, semantically coherent chunks.
| Splitter | Strategy | Best for |
|---|---|---|
RecursiveCharacter |
Descending separators (\n\n, \n, . , , char) |
General text |
Markdown |
Split on headers, preserve hierarchy | Markdown docs |
splitter = Ask::RAG::TextSplitter::RecursiveCharacter.new(
chunk_size: 1000,
chunk_overlap: 200
)
chunks = splitter.split_documents(docs)
# metadata[:chunk] tracks the chunk index per source document
splitter = Ask::RAG::TextSplitter::Markdown.new
chunks = splitter.split_text(markdown_content)
# Each chunk prepends header hierarchy: "Introduction > Background\n\ncontent..."
Vector Stores
Store embeddings and search by similarity.
| Store | Description | Dependencies |
|---|---|---|
InMemory |
Pure Ruby cosine similarity. Zero deps. | None |
PGVector (auto-loaded) |
PostgreSQL + pgvector extension | pgvector, activerecord |
# InMemory — no database needed
store = Ask::RAG::VectorStore::InMemory.new
store.add(documents, model: "text-embedding-3-small")
results = store.similarity_search("query", limit: 5)
# PGVector — for production Rails apps
store = Ask::RAG::VectorStore::PGVector.new(table_name: :embeddings)
store.add(documents, model: "text-embedding-3-small")
Search results have their similarity score in metadata[:score]:
results = store.similarity_search("authentication", limit: 3)
results.each do |doc|
puts "#{doc.[:score]} — #{doc.content[0..60]}"
end
Metadata filtering
Filter results by document metadata:
results = store.similarity_search(
"authentication",
filter: { source: "api_docs.md", version: "2.0" }
)
MMR (diversified search)
Avoid getting 5 near-identical chunks by applying Max Marginal Relevance:
results = store.similarity_search(
"authentication",
limit: 5,
mmr: true,
diversity_bonus: 0.3
)
# Results include both :score and :mmr_score in metadata
High-Level RAG Query
Retrieve + prompt + answer in one call:
answer = Ask::RAG::Query.query(
store: store,
question: "What is the default timeout configuration?",
model: "gpt-4o"
)
puts answer.content # The LLM's answer
puts answer.[:sources] # ["config.md", "settings.md"]
Pipeline (End-to-End)
# Load
loader = Ask::RAG::Loader::PDF.new
docs = loader.load("user_manual.pdf")
# Split
splitter = Ask::RAG::TextSplitter::RecursiveCharacter.new(
chunk_size: 500, chunk_overlap: 50
)
chunks = splitter.split_documents(docs)
# Store
store = Ask::RAG::VectorStore::InMemory.new
store.add(chunks, model: "text-embedding-3-small")
# Ask
answer = Ask::RAG::Query.query(
store: store,
question: "How do I reset my password?",
model: "gpt-4o"
)
puts answer.content
Integration with ask-agent
require "ask-rag"
require "ask-agent"
# Index your documentation
store = Ask::RAG::VectorStore::InMemory.new
loader = Ask::RAG::Loader::Markdown.new
splitter = Ask::RAG::TextSplitter::RecursiveCharacter.new(chunk_size: 1000)
docs = splitter.split_documents(loader.load("docs/"))
store.add(docs, model: "text-embedding-3-small")
# Give the agent a search tool
class SearchDocs < Ask::Tool
description "Search documentation for relevant information"
param :query, type: :string, desc: "Search query"
def execute(query:)
store.similarity_search(query, limit: 3).map do |doc|
{ content: doc.content[0..500], score: doc.[:score] }
end
end
end
session = Ask::Agent::Session.new(
model: "gpt-4o",
tools: [SearchDocs]
)
session.run("How do I configure authentication?")
Development
bundle exec rake test
License
MIT