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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ed27c04c401121b0c7f8fc5650abf78602e3f930914e48bafb2749894c3f3c0
4
- data.tar.gz: e7a4f56e7a9a0cc7b9930fb39e097898c812ba9a3e6ce702a28a4b51da915551
3
+ metadata.gz: b8c4c7c6f458001f016b74e56d5050632bc819d23c25e6b810c7900605b1d401
4
+ data.tar.gz: 3793d26b5fd103ca5ef0e32ff708f7fb235fbd5888d2f306db8f39416ab832f3
5
5
  SHA512:
6
- metadata.gz: 8eb13a7acab801fa0db31dc74d7a86caa9bee9cb423fa8646e44581673e6122d7f3e3a58deaf74635393fe763b48208f84976fa2931b1eab5a814a45f4134d5a
7
- data.tar.gz: cf1857fc42076bfaab4a687364ed21172badef86c366e2919a4b3aad748ee4f45946e8c4549b221f69769aa22b8e9e828939c3c7228741b97e1f49c34d2afc8a
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.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.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.binread(file_path))
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
- begin
337
- normalized = normalized.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: '')
338
- rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
339
- normalized = normalized.force_encoding(Encoding::UTF_8).scrub
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
- # For development/test environments, always fall back to simple if pg_jieba is not available
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?
@@ -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)
@@ -1,3 +1,3 @@
1
1
  module SmartRAG
2
- VERSION = "0.2.2"
2
+ VERSION = "0.2.4"
3
3
  end
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
- fulltext_manager = ::SmartRAG::Core::FulltextManager.new(db_connection, (@config[:fulltext] || {}).merge(logger: @logger))
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.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.16
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