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