ask-rag 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/lib/ask/rag/loader/directory.rb +2 -0
- data/lib/ask/rag/loader/json.rb +63 -0
- data/lib/ask/rag/vector_store/base.rb +11 -0
- data/lib/ask/rag/vector_store/in_memory.rb +14 -2
- data/lib/ask/rag/vector_store/pgvector.rb +23 -14
- data/lib/ask/rag/version.rb +1 -1
- data/lib/ask/rag.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b79936f65b8b4b8f7cbfc1667703b645df6f411e84e7ee543071e4fabe9e6015
|
|
4
|
+
data.tar.gz: 474bac5cbc7f1d3f26b3ec0115591947577a3957aff4733a815f3f96b6f63dd6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b7042d0c1f27aee6ceb4a21122f3c84691afb3b635a8b2f25f94a8c8cac3d633e09507f5a5b11347c241885016bdca1261be9322a0823034c4d97e11d8bce9e
|
|
7
|
+
data.tar.gz: 48b984042f7138262c13cd1458fa9191ed914909b35690b524cf3917e8837009644d71078908c47e1d1332b8895c4cee3ec5e453315442ef13091ff77420c6da
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
## [0.2.1] — 2026-07-26
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **PGVector model tracking** — same fix as InMemory: now tracks the embedding model used during `add` and reuses it for query embedding. Also raises `EmbeddingError` instead of silently returning a zero vector on failure.
|
|
6
|
+
|
|
7
|
+
- **PGVector document mutability** — no longer calls `define_singleton_method(:score)` on frozen `Ask::Document`. Score is now returned in `metadata[:score]` consistently with InMemory.
|
|
8
|
+
|
|
9
|
+
- **PGVector embed_query** — now accepts the query text as an argument (was hardcoded to embed the literal string "query").
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`similarity_search_by_vector`** — search using a raw vector instead of text. Available on both InMemory and PGVector stores. Accepts `limit:` and `filter:` params.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
store.similarity_search_by_vector([0.1, 0.2, ...], limit: 5)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **`Ask::RAG::Loader::JSON`** — loads JSON files. Supports arrays of objects (one document per object) and single objects. Configurable `content_key` and `metadata_keys`.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
loader = Ask::RAG::Loader::JSON.new(content_key: "body")
|
|
23
|
+
docs = loader.load("articles.json")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **Directory loader** now also recognizes `.json` files.
|
|
27
|
+
|
|
28
|
+
### Tested
|
|
29
|
+
|
|
30
|
+
- 65 tests, 163 assertions, 0 failures
|
|
31
|
+
|
|
1
32
|
## [0.2.0] — 2026-07-26
|
|
2
33
|
|
|
3
34
|
### Fixed
|
|
@@ -26,6 +26,7 @@ module Ask
|
|
|
26
26
|
".html" => :html,
|
|
27
27
|
".htm" => :html,
|
|
28
28
|
".csv" => :csv,
|
|
29
|
+
".json" => :json,
|
|
29
30
|
".pdf" => :pdf
|
|
30
31
|
}.freeze
|
|
31
32
|
|
|
@@ -96,6 +97,7 @@ module Ask
|
|
|
96
97
|
when :markdown then Loader::Markdown.new
|
|
97
98
|
when :html then defined?(Loader::HTML) ? Loader::HTML.new : nil
|
|
98
99
|
when :csv then Loader::CSV.new
|
|
100
|
+
when :json then Loader::JSON.new
|
|
99
101
|
when :pdf then defined?(Loader::PDF) ? Loader::PDF.new : nil
|
|
100
102
|
end
|
|
101
103
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module RAG
|
|
5
|
+
module Loader
|
|
6
|
+
# Loads a JSON file as documents.
|
|
7
|
+
#
|
|
8
|
+
# Supports two modes:
|
|
9
|
+
# 1. Array of objects — each object becomes one document.
|
|
10
|
+
# Specify +content_key+ and +metadata_keys+ to map fields.
|
|
11
|
+
# 2. Single object — becomes one document.
|
|
12
|
+
# Specify +content_key+ for which field holds the text.
|
|
13
|
+
#
|
|
14
|
+
# @example Array of objects
|
|
15
|
+
# # [{ "title": "...", "body": "...", "tags": [...] }, ...]
|
|
16
|
+
# loader = Ask::RAG::Loader::JSON.new(content_key: "body")
|
|
17
|
+
# docs = loader.load("articles.json")
|
|
18
|
+
#
|
|
19
|
+
# @example With metadata mapping
|
|
20
|
+
# loader = Ask::RAG::Loader::JSON.new(
|
|
21
|
+
# content_key: "content",
|
|
22
|
+
# metadata_keys: ["source", "date"]
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
class JSON < Base
|
|
26
|
+
# @param content_key [String] key whose value becomes document content
|
|
27
|
+
# @param metadata_keys [Array<String>, nil] keys to include in metadata
|
|
28
|
+
# (nil = all keys except content_key)
|
|
29
|
+
def initialize(content_key: "content", metadata_keys: nil)
|
|
30
|
+
@content_key = content_key
|
|
31
|
+
@metadata_keys = metadata_keys
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def load(path)
|
|
35
|
+
raw = ::JSON.parse(File.read(path))
|
|
36
|
+
items = raw.is_a?(Array) ? raw : [raw]
|
|
37
|
+
|
|
38
|
+
items.map { |item| build_document(item, path) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def build_document(item, path)
|
|
44
|
+
content = item[@content_key].to_s
|
|
45
|
+
metadata = { source: path }
|
|
46
|
+
|
|
47
|
+
if @metadata_keys
|
|
48
|
+
@metadata_keys.each do |key|
|
|
49
|
+
metadata[key] = item[key] if item.key?(key)
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
item.each do |key, value|
|
|
53
|
+
next if key == @content_key
|
|
54
|
+
metadata[key] = value if value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Ask::Document.new(content: content, metadata: metadata)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -41,6 +41,17 @@ module Ask
|
|
|
41
41
|
raise NotImplementedError
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Search by a raw vector instead of a text query.
|
|
45
|
+
# Useful when you've already embedded the query externally.
|
|
46
|
+
#
|
|
47
|
+
# @param vector [Array<Float>] the query vector
|
|
48
|
+
# @param limit [Integer] maximum number of results
|
|
49
|
+
# @param filter [Hash, nil] metadata filter
|
|
50
|
+
# @return [Array<Ask::Document>] documents with +:score+ in metadata
|
|
51
|
+
def similarity_search_by_vector(vector, limit: 10, filter: nil)
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
end
|
|
54
|
+
|
|
44
55
|
# Remove documents by ID.
|
|
45
56
|
# @param ids [Array<String>] document IDs to remove
|
|
46
57
|
def delete(ids)
|
|
@@ -81,12 +81,22 @@ module Ask
|
|
|
81
81
|
# @return [Array<Ask::Document>] documents with +:score+ and +:mmr_score+ in metadata
|
|
82
82
|
def similarity_search(query, limit: 10, filter: nil, mmr: false, diversity_bonus: 0.3)
|
|
83
83
|
query_vector = embed_query
|
|
84
|
+
search_by_vector(query_vector, limit: limit, filter: filter,
|
|
85
|
+
mmr: mmr, diversity_bonus: diversity_bonus)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def similarity_search_by_vector(vector, limit: 10, filter: nil)
|
|
89
|
+
search_by_vector(vector, limit: limit, filter: filter)
|
|
90
|
+
end
|
|
84
91
|
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def search_by_vector(vector, limit:, filter: nil, mmr: false, diversity_bonus: 0.3)
|
|
85
95
|
candidates = @mutex.synchronize do
|
|
86
96
|
@entries.filter_map do |_id, entry|
|
|
87
97
|
next if filter && !matches_filter?(entry.document.metadata, filter)
|
|
88
98
|
|
|
89
|
-
score = cosine_similarity(
|
|
99
|
+
score = cosine_similarity(vector, entry.vector)
|
|
90
100
|
[entry, score]
|
|
91
101
|
end
|
|
92
102
|
end
|
|
@@ -94,7 +104,7 @@ module Ask
|
|
|
94
104
|
candidates.sort_by! { |_, score| -score }
|
|
95
105
|
|
|
96
106
|
if mmr
|
|
97
|
-
apply_mmr(candidates,
|
|
107
|
+
apply_mmr(candidates, vector, limit, diversity_bonus)
|
|
98
108
|
else
|
|
99
109
|
candidates.first(limit).map do |entry, score|
|
|
100
110
|
build_result(entry, score: score)
|
|
@@ -102,6 +112,8 @@ module Ask
|
|
|
102
112
|
end
|
|
103
113
|
end
|
|
104
114
|
|
|
115
|
+
public
|
|
116
|
+
|
|
105
117
|
def delete(ids)
|
|
106
118
|
@mutex.synchronize do
|
|
107
119
|
ids.each { |id| @entries.delete(id) }
|
|
@@ -19,7 +19,7 @@ module Ask
|
|
|
19
19
|
#
|
|
20
20
|
# @example Usage
|
|
21
21
|
# store = Ask::RAG::VectorStore::PGVector.new(
|
|
22
|
-
# table_name: :
|
|
22
|
+
# table_name: :embeddings,
|
|
23
23
|
# embedding_column: :embedding
|
|
24
24
|
# )
|
|
25
25
|
# store.add(chunks, model: "text-embedding-3-small")
|
|
@@ -38,18 +38,19 @@ module Ask
|
|
|
38
38
|
@metadata_column = metadata_column.to_s
|
|
39
39
|
@embedding_column = embedding_column.to_s
|
|
40
40
|
@model_class = model_class || infer_model_class
|
|
41
|
+
@embedding_model = nil
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
def add(documents, model:, batch_size: 20)
|
|
44
45
|
ids = []
|
|
45
|
-
|
|
46
|
+
@embedding_model = model
|
|
46
47
|
|
|
47
48
|
documents.each_slice(batch_size) do |batch|
|
|
48
49
|
texts = batch.map(&:content)
|
|
49
50
|
vectors = embed_texts(texts, model)
|
|
50
51
|
|
|
51
52
|
batch.each_with_index do |doc, idx|
|
|
52
|
-
record = model_class.create!(
|
|
53
|
+
record = @model_class.create!(
|
|
53
54
|
@content_column => doc.content,
|
|
54
55
|
@metadata_column => doc.metadata,
|
|
55
56
|
@embedding_column => vectors[idx]
|
|
@@ -63,11 +64,16 @@ module Ask
|
|
|
63
64
|
|
|
64
65
|
def similarity_search(query, limit: 10, filter: nil, mmr: false, diversity_bonus: 0.3)
|
|
65
66
|
query_vector = embed_query(query)
|
|
67
|
+
similarity_search_by_vector(query_vector, limit: limit, filter: filter)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def similarity_search_by_vector(vector, limit: 10, filter: nil)
|
|
71
|
+
vector_str = vector.is_a?(Array) ? "[#{vector.join(',')}]" : vector.to_s
|
|
66
72
|
column = "#{@table_name}.#{@embedding_column}"
|
|
67
73
|
model_class = @model_class
|
|
68
74
|
|
|
69
75
|
scope = model_class
|
|
70
|
-
.select("#{@table_name}.*, 1 - (#{column} <=> '#{
|
|
76
|
+
.select("#{@table_name}.*, 1 - (#{column} <=> '#{vector_str}') AS score")
|
|
71
77
|
.where("#{column} IS NOT NULL")
|
|
72
78
|
|
|
73
79
|
if filter
|
|
@@ -77,16 +83,18 @@ module Ask
|
|
|
77
83
|
end
|
|
78
84
|
|
|
79
85
|
records = scope
|
|
80
|
-
.order(Arel.sql("#{column} <=> '#{
|
|
86
|
+
.order(Arel.sql("#{column} <=> '#{vector_str}'"))
|
|
81
87
|
.limit(limit)
|
|
82
88
|
|
|
83
89
|
records.map do |record|
|
|
84
|
-
|
|
90
|
+
Ask::Document.new(
|
|
85
91
|
content: record.send(@content_column),
|
|
86
|
-
metadata: (record.send(@metadata_column) || {}).merge(
|
|
92
|
+
metadata: (record.send(@metadata_column) || {}).merge(
|
|
93
|
+
score: record.score,
|
|
94
|
+
db_id: record.id
|
|
95
|
+
),
|
|
96
|
+
id: record.id.to_s
|
|
87
97
|
)
|
|
88
|
-
doc.define_singleton_method(:score) { record.score }
|
|
89
|
-
doc
|
|
90
98
|
end
|
|
91
99
|
end
|
|
92
100
|
|
|
@@ -117,13 +125,14 @@ module Ask
|
|
|
117
125
|
Array(raw).map { |v| Array(v).map(&:to_f) }
|
|
118
126
|
end
|
|
119
127
|
|
|
120
|
-
def embed_query(query)
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
def embed_query(query = "query")
|
|
129
|
+
model = @embedding_model || "text-embedding-3-small"
|
|
130
|
+
provider = resolve_provider(model)
|
|
131
|
+
raw = provider.embed(query, model: model)
|
|
123
132
|
raw = raw.output if raw.is_a?(Ask::Result)
|
|
124
133
|
Array(raw).flatten.map(&:to_f)
|
|
125
|
-
rescue StandardError
|
|
126
|
-
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
raise EmbeddingError, "Failed to embed query with model #{model}: #{e.message}"
|
|
127
136
|
end
|
|
128
137
|
|
|
129
138
|
def resolve_provider(model)
|
data/lib/ask/rag/version.rb
CHANGED
data/lib/ask/rag.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "rag/loader/base"
|
|
|
14
14
|
require_relative "rag/loader/text"
|
|
15
15
|
require_relative "rag/loader/markdown"
|
|
16
16
|
require_relative "rag/loader/csv"
|
|
17
|
+
require_relative "rag/loader/json"
|
|
17
18
|
require_relative "rag/loader/directory"
|
|
18
19
|
|
|
19
20
|
# Optional loaders (loaded only when their dependency gem is available)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-rag
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -111,6 +111,7 @@ files:
|
|
|
111
111
|
- lib/ask/rag/loader/csv.rb
|
|
112
112
|
- lib/ask/rag/loader/directory.rb
|
|
113
113
|
- lib/ask/rag/loader/html.rb
|
|
114
|
+
- lib/ask/rag/loader/json.rb
|
|
114
115
|
- lib/ask/rag/loader/markdown.rb
|
|
115
116
|
- lib/ask/rag/loader/pdf.rb
|
|
116
117
|
- lib/ask/rag/loader/text.rb
|