smart_rag 0.2.2 → 0.2.4
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/Gemfile.lock +2 -2
- data/lib/smart_rag/core/document_processor.rb +18 -5
- data/lib/smart_rag/core/fulltext_manager.rb +58 -13
- data/lib/smart_rag/retrieve.rb +31 -0
- data/lib/smart_rag/version.rb +1 -1
- data/lib/smart_rag.rb +7 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8c4c7c6f458001f016b74e56d5050632bc819d23c25e6b810c7900605b1d401
|
|
4
|
+
data.tar.gz: 3793d26b5fd103ca5ef0e32ff708f7fb235fbd5888d2f306db8f39416ab832f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 45d530acad3be58c9126d61bc426780873ab03dd9129cbb4b8220b8d9655012ab4e2ae9304b813ebd9a334f0dc444c237c4ab46166edb182582f39db4ceefbc9
|
|
7
|
+
data.tar.gz: ed4c245e61190954e33460679faacc13b2d2ac6c8d23b296f09b7325e86aa899a515c4b1147bf1b9d56750b7616115292a12d3f198cf004c917ffd415fe04418
|
data/Gemfile.lock
CHANGED
|
@@ -7,7 +7,7 @@ GIT
|
|
|
7
7
|
PATH
|
|
8
8
|
remote: .
|
|
9
9
|
specs:
|
|
10
|
-
smart_rag (0.2.
|
|
10
|
+
smart_rag (0.2.3)
|
|
11
11
|
aws-sdk-s3 (~> 1.0)
|
|
12
12
|
concurrent-ruby (~> 1.0)
|
|
13
13
|
dotenv (~> 2.8)
|
|
@@ -381,7 +381,7 @@ CHECKSUMS
|
|
|
381
381
|
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
|
382
382
|
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
|
|
383
383
|
smart_prompt (0.5.4) sha256=81d5e052cf05d63051a68e946e0ba25bcd280dca5e43ce96664551a33656e5bc
|
|
384
|
-
smart_rag (0.2.
|
|
384
|
+
smart_rag (0.2.3)
|
|
385
385
|
standardwebhooks (1.1.0) sha256=356ba711c14def4fb27310e4163b26bd5cccf325212c3440cabadd825e4be11f
|
|
386
386
|
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
|
387
387
|
tiktoken_ruby (0.0.17-x64-mingw-ucrt) sha256=8bd2f8e2c38ee9052a8cd4fc83fcb41362beb476d8885ac121125cd4dd065596
|
|
@@ -321,8 +321,15 @@ module SmartRAG
|
|
|
321
321
|
raise e
|
|
322
322
|
end
|
|
323
323
|
|
|
324
|
+
# Read a text file as UTF-8.
|
|
325
|
+
#
|
|
326
|
+
# File.binread returns an ASCII-8BIT string, and transcoding that to UTF-8
|
|
327
|
+
# with `undef: :replace, replace: ''` silently deletes every high byte —
|
|
328
|
+
# i.e. it deletes all non-ASCII text (CJK included). Declare the encoding
|
|
329
|
+
# of the bytes instead of transcoding from binary; `scrub` then replaces
|
|
330
|
+
# only genuinely invalid byte sequences.
|
|
324
331
|
def read_text_file(file_path)
|
|
325
|
-
normalize_text_content(File.
|
|
332
|
+
normalize_text_content(File.read(file_path, mode: 'rb').force_encoding(Encoding::UTF_8))
|
|
326
333
|
rescue StandardError => e
|
|
327
334
|
@logger.error "Failed to read text file #{file_path}: #{e.message}"
|
|
328
335
|
raise e
|
|
@@ -333,10 +340,16 @@ module SmartRAG
|
|
|
333
340
|
|
|
334
341
|
normalized = content.is_a?(String) ? content.dup : content.to_s
|
|
335
342
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
normalized = normalized.force_encoding(Encoding::UTF_8)
|
|
343
|
+
if normalized.encoding == Encoding::ASCII_8BIT
|
|
344
|
+
# Bytes of unknown encoding: reinterpret as UTF-8 instead of dropping
|
|
345
|
+
# everything above 0x7F.
|
|
346
|
+
normalized = normalized.force_encoding(Encoding::UTF_8)
|
|
347
|
+
elsif normalized.encoding != Encoding::UTF_8
|
|
348
|
+
begin
|
|
349
|
+
normalized = normalized.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: '')
|
|
350
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
|
351
|
+
normalized = normalized.force_encoding(Encoding::UTF_8)
|
|
352
|
+
end
|
|
340
353
|
end
|
|
341
354
|
|
|
342
355
|
normalized.scrub
|
|
@@ -28,6 +28,13 @@ module SmartRAG
|
|
|
28
28
|
@query_parser = options[:query_parser] || Parsers::QueryParser.new
|
|
29
29
|
@logger = options[:logger] || Logger.new(STDOUT)
|
|
30
30
|
@config = DEFAULT_CONFIG.merge(options)
|
|
31
|
+
|
|
32
|
+
# Callers either hand over the whole configuration (with a nested
|
|
33
|
+
# `fulltext_search` section) or the section itself. Surface the nested
|
|
34
|
+
# keys either way so options like `enable_jieba` are actually visible.
|
|
35
|
+
nested = @config[:fulltext_search] || @config['fulltext_search'] ||
|
|
36
|
+
@config[:fulltext] || @config['fulltext']
|
|
37
|
+
@config = @config.merge(nested) if nested.is_a?(Hash)
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
# Store or update full-text index for a section
|
|
@@ -345,29 +352,67 @@ module SmartRAG
|
|
|
345
352
|
dataset
|
|
346
353
|
end
|
|
347
354
|
|
|
355
|
+
# Text search configurations shipped by the pg_jieba extension: upstream
|
|
356
|
+
# installs `jiebacfg` (mix segmentation, recommended) plus `jiebaqry`,
|
|
357
|
+
# `jiebamp` and `jiebahmm`. No released version provides a configuration
|
|
358
|
+
# literally named `jieba` — probing that name always failed, which silently
|
|
359
|
+
# downgraded Chinese search to `simple` even with the extension installed.
|
|
360
|
+
JIEBA_CONFIG = 'jiebacfg'
|
|
361
|
+
JIEBA_CONFIG_NAMES = %w[jieba jiebacfg jiebaqry jiebamp jiebahmm].freeze
|
|
362
|
+
|
|
348
363
|
# Get text search configuration
|
|
349
364
|
def get_text_search_config(language)
|
|
350
365
|
config = Models::TextSearchConfig.first(language_code: language.to_s)&.config_name
|
|
351
366
|
return 'pg_catalog.simple' unless config
|
|
367
|
+
return config unless jieba_config?(config)
|
|
368
|
+
# `fulltext_search.enable_jieba` (on unless explicitly disabled) is the
|
|
369
|
+
# switch for Chinese segmentation.
|
|
370
|
+
return 'pg_catalog.simple' unless jieba_enabled?
|
|
352
371
|
|
|
353
|
-
|
|
354
|
-
if config == 'jieba'
|
|
355
|
-
begin
|
|
356
|
-
# Test if pg_jieba is available in a separate transaction
|
|
357
|
-
db.fetch("SELECT to_tsvector('jieba', 'test')").first
|
|
358
|
-
return 'jieba'
|
|
359
|
-
rescue StandardError => e
|
|
360
|
-
@logger.warn "pg_jieba extension not available, falling back to simple: #{e.message}"
|
|
361
|
-
return 'pg_catalog.simple'
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
config
|
|
372
|
+
jieba_config_name(config) || 'pg_catalog.simple'
|
|
366
373
|
rescue StandardError => e
|
|
367
374
|
@logger.warn "Failed to get text search config for #{language}: #{e.message}, using simple"
|
|
368
375
|
'pg_catalog.simple'
|
|
369
376
|
end
|
|
370
377
|
|
|
378
|
+
# Does this configuration name belong to pg_jieba?
|
|
379
|
+
def jieba_config?(name)
|
|
380
|
+
JIEBA_CONFIG_NAMES.include?(name.to_s.sub(/\Apg_catalog\./, ''))
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# `enable_jieba` defaults to on; only an explicit false switches it off.
|
|
384
|
+
def jieba_enabled?
|
|
385
|
+
value = config_value(:enable_jieba)
|
|
386
|
+
return true if value.nil?
|
|
387
|
+
|
|
388
|
+
value != false && value.to_s != 'false'
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Return a configuration the server actually has: prefer the name the
|
|
392
|
+
# database row specifies, then the name pg_jieba really installs.
|
|
393
|
+
#
|
|
394
|
+
# This asks the catalogue instead of probing with `to_tsvector(name, ...)`:
|
|
395
|
+
# a probe that raises would abort any enclosing transaction (PostgreSQL
|
|
396
|
+
# refuses further statements until it is rolled back), which made every
|
|
397
|
+
# lookup after the first failure fall back to simple.
|
|
398
|
+
def jieba_config_name(preferred)
|
|
399
|
+
candidates = [preferred.to_s, JIEBA_CONFIG].uniq
|
|
400
|
+
present = db[:pg_ts_config].where(cfgname: candidates).select_map(:cfgname)
|
|
401
|
+
name = candidates.find { |candidate| present.include?(candidate) }
|
|
402
|
+
return name if name
|
|
403
|
+
|
|
404
|
+
@logger.warn "pg_jieba text search configuration unavailable " \
|
|
405
|
+
"(tried: #{candidates.join(', ')}), falling back to simple"
|
|
406
|
+
nil
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def config_value(key)
|
|
410
|
+
return @config[key] if @config.key?(key)
|
|
411
|
+
return @config[key.to_s] if @config.key?(key.to_s)
|
|
412
|
+
|
|
413
|
+
nil
|
|
414
|
+
end
|
|
415
|
+
|
|
371
416
|
# Set weight for tsvector (helper method)
|
|
372
417
|
def setweight(vector, weight)
|
|
373
418
|
return '' if vector.nil? || vector.to_s.strip.empty?
|
data/lib/smart_rag/retrieve.rb
CHANGED
|
@@ -19,6 +19,7 @@ module SmartRAG
|
|
|
19
19
|
def execute(plan:)
|
|
20
20
|
@document_metadata_cache = {}
|
|
21
21
|
@section_topic_cache = {}
|
|
22
|
+
@document_record_cache = {}
|
|
22
23
|
normalized_plan = normalize_plan(plan)
|
|
23
24
|
validate_plan!(normalized_plan)
|
|
24
25
|
principal = normalized_plan.delete(:_principal)
|
|
@@ -459,6 +460,15 @@ module SmartRAG
|
|
|
459
460
|
return section.document.url if section.document.respond_to?(:url)
|
|
460
461
|
end
|
|
461
462
|
|
|
463
|
+
# Not every search backend returns source metadata: vector search results
|
|
464
|
+
# carry only similarity/rank/section, so this returned nil for them. A nil
|
|
465
|
+
# URI matches no `source_uri_prefix`, so every candidate was rejected by
|
|
466
|
+
# the scope filter and semantic/hybrid plans always came back empty. The
|
|
467
|
+
# document row is authoritative.
|
|
468
|
+
document = document_record_for(extract_document_id(result, section))
|
|
469
|
+
stored_uri = document && document[:source_uri]
|
|
470
|
+
return stored_uri unless stored_uri.to_s.empty?
|
|
471
|
+
|
|
462
472
|
nil
|
|
463
473
|
end
|
|
464
474
|
|
|
@@ -471,6 +481,12 @@ module SmartRAG
|
|
|
471
481
|
end
|
|
472
482
|
end
|
|
473
483
|
|
|
484
|
+
# Same reasoning as extract_source_uri: prefer what was stored on the
|
|
485
|
+
# document over guessing from a URI a backend may not have returned.
|
|
486
|
+
document = document_record_for(extract_document_id(result, extract_section(result)))
|
|
487
|
+
stored_type = document && document[:source_type]
|
|
488
|
+
return stored_type unless stored_type.to_s.empty?
|
|
489
|
+
|
|
474
490
|
return 'url' if source_uri.to_s.start_with?('http://', 'https://')
|
|
475
491
|
return 'file' if source_uri.to_s.start_with?('file://', '/')
|
|
476
492
|
|
|
@@ -578,6 +594,21 @@ module SmartRAG
|
|
|
578
594
|
nil
|
|
579
595
|
end
|
|
580
596
|
|
|
597
|
+
# Document rows are the authoritative source for scope metadata; search
|
|
598
|
+
# backends do not all echo it back to the caller.
|
|
599
|
+
def document_record_for(document_id)
|
|
600
|
+
return nil if document_id.nil?
|
|
601
|
+
return nil unless defined?(::SmartRAG) && ::SmartRAG.respond_to?(:db) && ::SmartRAG.db
|
|
602
|
+
|
|
603
|
+
@document_record_cache ||= {}
|
|
604
|
+
key = document_id.to_i
|
|
605
|
+
return @document_record_cache[key] if @document_record_cache.key?(key)
|
|
606
|
+
|
|
607
|
+
@document_record_cache[key] = ::SmartRAG.db[:source_documents].where(id: key).first
|
|
608
|
+
rescue StandardError
|
|
609
|
+
nil
|
|
610
|
+
end
|
|
611
|
+
|
|
581
612
|
def extract_vector_score(result, mode)
|
|
582
613
|
return numeric_or_nil(result[:vector_score]) if result.is_a?(Hash) && result.key?(:vector_score)
|
|
583
614
|
return numeric_or_nil(result[:similarity]) if mode == 'semantic' && result.is_a?(Hash) && result.key?(:similarity)
|
data/lib/smart_rag/version.rb
CHANGED
data/lib/smart_rag.rb
CHANGED
|
@@ -1026,7 +1026,13 @@ module SmartRAG
|
|
|
1026
1026
|
db_connection = ::SmartRAG.db
|
|
1027
1027
|
|
|
1028
1028
|
embedding_manager = ::SmartRAG::Core::Embedding.new(@config[:database])
|
|
1029
|
-
|
|
1029
|
+
# `fulltext_search` is the section name used by the configuration files;
|
|
1030
|
+
# it used to be read as `fulltext`, so the whole section (including
|
|
1031
|
+
# `enable_jieba`) never reached FulltextManager.
|
|
1032
|
+
fulltext_section = @config[:fulltext_search] || @config[:fulltext] || {}
|
|
1033
|
+
fulltext_manager = ::SmartRAG::Core::FulltextManager.new(
|
|
1034
|
+
db_connection, fulltext_section.merge(logger: @logger)
|
|
1035
|
+
)
|
|
1030
1036
|
|
|
1031
1037
|
@query_processor = ::SmartRAG::Core::QueryProcessor.new(
|
|
1032
1038
|
config: @config,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smart_rag
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SmartRAG Team
|
|
@@ -417,7 +417,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
417
417
|
- !ruby/object:Gem::Version
|
|
418
418
|
version: '0'
|
|
419
419
|
requirements: []
|
|
420
|
-
rubygems_version: 4.0.
|
|
420
|
+
rubygems_version: 4.0.20
|
|
421
421
|
specification_version: 4
|
|
422
422
|
summary: A hybrid RAG (Retrieval-Augmented Generation) system with vector and full-text
|
|
423
423
|
search
|