vectra-client 0.4.0 → 1.0.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.
@@ -4,34 +4,34 @@
4
4
 
5
5
  /* CSS Custom Properties */
6
6
  :root {
7
- /* Color Palette - Clean Green Accent */
8
- --tma-color-bg-primary: #0a0a0f;
9
- --tma-color-bg-secondary: #111118;
10
- --tma-color-bg-tertiary: #1a1a22;
11
- --tma-color-bg-elevated: #222230;
12
- --tma-color-bg-hover: #2a2a3a;
7
+ /* Color Palette - Deep Navy + Coral Accent */
8
+ --tma-color-bg-primary: #0c1a3f;
9
+ --tma-color-bg-secondary: #111f4b;
10
+ --tma-color-bg-tertiary: #17265c;
11
+ --tma-color-bg-elevated: #1f2f70;
12
+ --tma-color-bg-hover: #24367d;
13
13
 
14
- /* Accent Colors - Based on #05df72 */
15
- --tma-color-accent-primary: #05df72;
16
- --tma-color-accent-light: #2ee889;
17
- --tma-color-accent-dark: #04b85e;
18
- --tma-color-accent-muted: rgba(5, 223, 114, 0.15);
14
+ /* Accent Colors - Based on logo (#e19e96) */
15
+ --tma-color-accent-primary: #e19e96;
16
+ --tma-color-accent-light: #f1b4aa;
17
+ --tma-color-accent-dark: #c46f63;
18
+ --tma-color-accent-muted: rgba(225, 158, 150, 0.16);
19
19
 
20
20
  /* Text Colors */
21
21
  --tma-color-text-primary: #f4f4f5;
22
- --tma-color-text-secondary: #a1a1aa;
23
- --tma-color-text-muted: #71717a;
24
- --tma-color-text-accent: #05df72;
22
+ --tma-color-text-secondary: #c1c2d0;
23
+ --tma-color-text-muted: #8b8ca0;
24
+ --tma-color-text-accent: #e19e96;
25
25
 
26
26
  /* Border Colors */
27
27
  --tma-color-border: rgba(255, 255, 255, 0.08);
28
- --tma-color-border-hover: rgba(5, 223, 114, 0.4);
28
+ --tma-color-border-hover: rgba(225, 158, 150, 0.6);
29
29
 
30
30
  /* Shadows */
31
31
  --tma-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
32
32
  --tma-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
33
33
  --tma-shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.6);
34
- --tma-shadow-glow: 0 0 30px rgba(5, 223, 114, 0.2);
34
+ --tma-shadow-glow: 0 0 30px rgba(225, 158, 150, 0.35);
35
35
 
36
36
  /* Typography */
37
37
  --tma-font-family-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
@@ -98,7 +98,7 @@ body {
98
98
  left: 0;
99
99
  right: 0;
100
100
  height: var(--tma-header-height);
101
- background: rgba(10, 10, 15, 0.95);
101
+ background: rgba(12, 26, 63, 0.96);
102
102
  backdrop-filter: blur(12px);
103
103
  border-bottom: 1px solid var(--tma-color-border);
104
104
  z-index: 1000;
@@ -119,15 +119,16 @@ body {
119
119
  align-items: center;
120
120
  gap: var(--tma-spacing-sm);
121
121
  text-decoration: none;
122
- font-size: 1.5rem;
123
- font-weight: 800;
124
- color: var(--tma-color-accent-primary);
125
- letter-spacing: -0.02em;
122
+ color: var(--tma-color-text-primary);
126
123
  }
127
124
 
128
125
  .tma-nav__brand::before {
129
- content: '';
130
- font-size: 1.2rem;
126
+ content: '';
127
+ }
128
+
129
+ .tma-nav__logo {
130
+ width: 110px;
131
+ display: block;
131
132
  }
132
133
 
133
134
  .tma-nav__menu {
@@ -220,7 +221,7 @@ body {
220
221
  gap: var(--tma-spacing-sm);
221
222
  padding: var(--tma-spacing-xs) var(--tma-spacing-md);
222
223
  background: var(--tma-color-accent-muted);
223
- border: 1px solid rgba(5, 223, 114, 0.3);
224
+ border: 1px solid rgba(225, 158, 150, 0.3);
224
225
  border-radius: 100px;
225
226
  font-size: 0.85rem;
226
227
  font-weight: 500;
@@ -696,6 +697,370 @@ code {
696
697
  color: var(--tma-color-text-secondary);
697
698
  }
698
699
 
700
+ /* Comparison Table Styles */
701
+ .tma-comparison {
702
+ padding: var(--tma-spacing-3xl) var(--tma-spacing-xl);
703
+ max-width: var(--tma-max-width);
704
+ margin: 0 auto;
705
+ }
706
+
707
+ .tma-comparison-table {
708
+ width: 100%;
709
+ border-collapse: collapse;
710
+ margin: var(--tma-spacing-xl) 0;
711
+ background: var(--tma-color-bg-secondary);
712
+ border-radius: var(--tma-radius-lg);
713
+ overflow: hidden;
714
+ border: 1px solid var(--tma-color-border);
715
+ }
716
+
717
+ .tma-comparison-table thead {
718
+ background: var(--tma-color-bg-tertiary);
719
+ }
720
+
721
+ .tma-comparison-table th {
722
+ padding: var(--tma-spacing-md) var(--tma-spacing-lg);
723
+ text-align: left;
724
+ font-weight: 600;
725
+ color: var(--tma-color-text-primary);
726
+ font-size: 0.9rem;
727
+ border-bottom: 2px solid var(--tma-color-border);
728
+ }
729
+
730
+ .tma-comparison-table th:first-child {
731
+ font-weight: 700;
732
+ color: var(--tma-color-accent-primary);
733
+ }
734
+
735
+ .tma-comparison-table td {
736
+ padding: var(--tma-spacing-md) var(--tma-spacing-lg);
737
+ border-bottom: 1px solid var(--tma-color-border);
738
+ color: var(--tma-color-text-secondary);
739
+ font-size: 0.9rem;
740
+ }
741
+
742
+ .tma-comparison-table tbody tr:hover {
743
+ background: var(--tma-color-bg-hover);
744
+ }
745
+
746
+ .tma-comparison-table tbody tr:last-child td {
747
+ border-bottom: none;
748
+ }
749
+
750
+ .tma-comparison-table .tma-check {
751
+ color: var(--tma-color-accent-primary);
752
+ font-weight: 600;
753
+ }
754
+
755
+ .tma-comparison-table .tma-partial {
756
+ color: #fbbf24;
757
+ font-weight: 500;
758
+ }
759
+
760
+ .tma-comparison-table .tma-cross {
761
+ color: var(--tma-color-text-muted);
762
+ }
763
+
764
+ /* Advantages Section */
765
+ .tma-advantages {
766
+ padding: var(--tma-spacing-3xl) var(--tma-spacing-xl);
767
+ max-width: var(--tma-max-width);
768
+ margin: 0 auto;
769
+ background: var(--tma-color-bg-secondary);
770
+ }
771
+
772
+ .tma-advantages__grid {
773
+ display: grid;
774
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
775
+ gap: var(--tma-spacing-lg);
776
+ margin-top: var(--tma-spacing-xl);
777
+ }
778
+
779
+ .tma-advantage-card {
780
+ padding: var(--tma-spacing-xl);
781
+ background: var(--tma-color-bg-tertiary);
782
+ border: 1px solid var(--tma-color-border);
783
+ border-radius: var(--tma-radius-lg);
784
+ border-left: 3px solid var(--tma-color-accent-primary);
785
+ }
786
+
787
+ .tma-advantage-card__title {
788
+ font-size: 1.1rem;
789
+ font-weight: 700;
790
+ margin-bottom: var(--tma-spacing-md);
791
+ color: var(--tma-color-text-primary);
792
+ }
793
+
794
+ .tma-advantage-card__list {
795
+ list-style: none;
796
+ padding: 0;
797
+ }
798
+
799
+ .tma-advantage-card__list li {
800
+ padding: var(--tma-spacing-xs) 0;
801
+ color: var(--tma-color-text-secondary);
802
+ font-size: 0.9rem;
803
+ }
804
+
805
+ .tma-advantage-card__list li::before {
806
+ content: '✓';
807
+ color: var(--tma-color-accent-primary);
808
+ font-weight: 700;
809
+ margin-right: var(--tma-spacing-sm);
810
+ }
811
+
812
+ /* Choose Section */
813
+ .tma-choose {
814
+ padding: var(--tma-spacing-3xl) var(--tma-spacing-xl);
815
+ max-width: var(--tma-max-width);
816
+ margin: 0 auto;
817
+ }
818
+
819
+ .tma-choose__grid {
820
+ display: grid;
821
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
822
+ gap: var(--tma-spacing-lg);
823
+ margin-top: var(--tma-spacing-xl);
824
+ }
825
+
826
+ .tma-choose-card {
827
+ padding: var(--tma-spacing-xl);
828
+ background: var(--tma-color-bg-secondary);
829
+ border: 1px solid var(--tma-color-border);
830
+ border-radius: var(--tma-radius-lg);
831
+ }
832
+
833
+ .tma-choose-card__title {
834
+ font-size: 1.1rem;
835
+ font-weight: 700;
836
+ margin-bottom: var(--tma-spacing-md);
837
+ color: var(--tma-color-accent-primary);
838
+ }
839
+
840
+ .tma-choose-card__list {
841
+ list-style: none;
842
+ padding: 0;
843
+ }
844
+
845
+ .tma-choose-card__list li {
846
+ padding: var(--tma-spacing-xs) 0;
847
+ color: var(--tma-color-text-secondary);
848
+ font-size: 0.9rem;
849
+ padding-left: var(--tma-spacing-md);
850
+ position: relative;
851
+ }
852
+
853
+ .tma-choose-card__list li::before {
854
+ content: '→';
855
+ position: absolute;
856
+ left: 0;
857
+ color: var(--tma-color-accent-primary);
858
+ }
859
+
860
+ /* Visual Charts Section */
861
+ .tma-charts {
862
+ padding: var(--tma-spacing-3xl) var(--tma-spacing-xl);
863
+ max-width: var(--tma-max-width);
864
+ margin: 0 auto;
865
+ background: var(--tma-color-bg-secondary);
866
+ }
867
+
868
+ .tma-charts__grid {
869
+ display: grid;
870
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
871
+ gap: var(--tma-spacing-2xl);
872
+ margin-top: var(--tma-spacing-xl);
873
+ }
874
+
875
+ .tma-chart-card {
876
+ padding: var(--tma-spacing-xl);
877
+ background: var(--tma-color-bg-tertiary);
878
+ border: 1px solid var(--tma-color-border);
879
+ border-radius: var(--tma-radius-lg);
880
+ }
881
+
882
+ .tma-chart-card__title {
883
+ font-size: 1.2rem;
884
+ font-weight: 700;
885
+ margin-bottom: var(--tma-spacing-lg);
886
+ color: var(--tma-color-text-primary);
887
+ text-align: center;
888
+ }
889
+
890
+ /* Bar Chart Styles */
891
+ .tma-bar-chart {
892
+ margin-top: var(--tma-spacing-lg);
893
+ }
894
+
895
+ .tma-bar-chart__item {
896
+ margin-bottom: var(--tma-spacing-md);
897
+ }
898
+
899
+ .tma-bar-chart__label {
900
+ display: flex;
901
+ justify-content: space-between;
902
+ margin-bottom: var(--tma-spacing-xs);
903
+ font-size: 0.9rem;
904
+ color: var(--tma-color-text-secondary);
905
+ }
906
+
907
+ .tma-bar-chart__name {
908
+ font-weight: 600;
909
+ color: var(--tma-color-text-primary);
910
+ }
911
+
912
+ .tma-bar-chart__value {
913
+ color: var(--tma-color-text-secondary);
914
+ }
915
+
916
+ .tma-bar-chart__bar-container {
917
+ height: 28px;
918
+ background: var(--tma-color-bg-primary);
919
+ border-radius: var(--tma-radius-sm);
920
+ overflow: hidden;
921
+ border: 1px solid var(--tma-color-border);
922
+ }
923
+
924
+ .tma-bar-chart__bar {
925
+ height: 100%;
926
+ background: linear-gradient(90deg, var(--tma-color-accent-primary), var(--tma-color-accent-light));
927
+ border-radius: var(--tma-radius-sm);
928
+ transition: width var(--tma-transition-slow);
929
+ display: flex;
930
+ align-items: center;
931
+ padding: 0 var(--tma-spacing-sm);
932
+ color: #0a0a0f;
933
+ font-weight: 600;
934
+ font-size: 0.85rem;
935
+ min-width: fit-content;
936
+ }
937
+
938
+ .tma-bar-chart__bar--vectra {
939
+ background: linear-gradient(90deg, var(--tma-color-accent-primary), var(--tma-color-accent-light));
940
+ }
941
+
942
+ .tma-bar-chart__bar--langchain {
943
+ background: linear-gradient(90deg, #fbbf24, #fcd34d);
944
+ }
945
+
946
+ .tma-bar-chart__bar--pgvector {
947
+ background: linear-gradient(90deg, #60a5fa, #93c5fd);
948
+ }
949
+
950
+ .tma-bar-chart__bar--qdrant {
951
+ background: linear-gradient(90deg, #a78bfa, #c4b5fd);
952
+ }
953
+
954
+ /* Radar Chart Styles */
955
+ .tma-radar-chart {
956
+ display: flex;
957
+ justify-content: center;
958
+ align-items: center;
959
+ margin: var(--tma-spacing-xl) 0;
960
+ }
961
+
962
+ .tma-radar-chart__container {
963
+ position: relative;
964
+ width: 300px;
965
+ height: 300px;
966
+ }
967
+
968
+ .tma-radar-chart__svg {
969
+ width: 100%;
970
+ height: 100%;
971
+ transform: rotate(-90deg);
972
+ }
973
+
974
+ .tma-radar-chart__axis {
975
+ stroke: var(--tma-color-border);
976
+ stroke-width: 1;
977
+ }
978
+
979
+ .tma-radar-chart__grid {
980
+ stroke: var(--tma-color-border);
981
+ stroke-width: 0.5;
982
+ fill: none;
983
+ }
984
+
985
+ .tma-radar-chart__area {
986
+ fill: var(--tma-color-accent-muted);
987
+ stroke: var(--tma-color-accent-primary);
988
+ stroke-width: 2;
989
+ opacity: 0.6;
990
+ }
991
+
992
+ .tma-radar-chart__point {
993
+ fill: var(--tma-color-accent-primary);
994
+ r: 4;
995
+ }
996
+
997
+ .tma-radar-chart__label {
998
+ font-size: 0.75rem;
999
+ fill: var(--tma-color-text-secondary);
1000
+ text-anchor: middle;
1001
+ transform: rotate(90deg);
1002
+ }
1003
+
1004
+ .tma-radar-chart__labels {
1005
+ position: absolute;
1006
+ top: 0;
1007
+ left: 0;
1008
+ right: 0;
1009
+ bottom: 0;
1010
+ display: flex;
1011
+ flex-direction: column;
1012
+ justify-content: space-around;
1013
+ padding: var(--tma-spacing-md);
1014
+ pointer-events: none;
1015
+ }
1016
+
1017
+ .tma-radar-chart__label-item {
1018
+ font-size: 0.8rem;
1019
+ color: var(--tma-color-text-secondary);
1020
+ text-align: center;
1021
+ font-weight: 500;
1022
+ }
1023
+
1024
+ /* Responsive Charts */
1025
+ @media (max-width: 768px) {
1026
+ .tma-charts__grid {
1027
+ grid-template-columns: 1fr;
1028
+ }
1029
+
1030
+ .tma-radar-chart__container {
1031
+ width: 250px;
1032
+ height: 250px;
1033
+ }
1034
+ }
1035
+
1036
+ /* Responsive Comparison Table */
1037
+ @media (max-width: 1024px) {
1038
+ .tma-comparison-table {
1039
+ font-size: 0.85rem;
1040
+ }
1041
+
1042
+ .tma-comparison-table th,
1043
+ .tma-comparison-table td {
1044
+ padding: var(--tma-spacing-sm) var(--tma-spacing-md);
1045
+ }
1046
+ }
1047
+
1048
+ @media (max-width: 768px) {
1049
+ .tma-comparison-table {
1050
+ font-size: 0.75rem;
1051
+ }
1052
+
1053
+ .tma-comparison-table th,
1054
+ .tma-comparison-table td {
1055
+ padding: var(--tma-spacing-xs) var(--tma-spacing-sm);
1056
+ }
1057
+
1058
+ .tma-advantages__grid,
1059
+ .tma-choose__grid {
1060
+ grid-template-columns: 1fr;
1061
+ }
1062
+ }
1063
+
699
1064
  /* ============================================
700
1065
  Footer
701
1066
  ============================================ */
@@ -121,6 +121,52 @@ if status[:error]
121
121
  end
122
122
  ```
123
123
 
124
+ ### Hybrid Search (Semantic + Keyword)
125
+
126
+ Combine the best of both worlds: semantic understanding from vectors and exact keyword matching:
127
+
128
+ ```ruby
129
+ # Hybrid search with 70% semantic, 30% keyword
130
+ results = client.hybrid_search(
131
+ index: 'docs',
132
+ vector: embedding, # Semantic search
133
+ text: 'ruby programming', # Keyword search
134
+ alpha: 0.7, # 0.0 = pure keyword, 1.0 = pure semantic
135
+ top_k: 10
136
+ )
137
+
138
+ results.each do |match|
139
+ puts "#{match.id}: #{match.score}"
140
+ end
141
+
142
+ # Pure semantic (alpha = 1.0)
143
+ results = client.hybrid_search(
144
+ index: 'docs',
145
+ vector: embedding,
146
+ text: 'ruby',
147
+ alpha: 1.0
148
+ )
149
+
150
+ # Pure keyword (alpha = 0.0)
151
+ results = client.hybrid_search(
152
+ index: 'docs',
153
+ vector: embedding,
154
+ text: 'ruby programming',
155
+ alpha: 0.0
156
+ )
157
+ ```
158
+
159
+ **Provider Support:**
160
+ - **Qdrant**: ✅ Full support (prefetch + rescore API)
161
+ - **Weaviate**: ✅ Full support (hybrid GraphQL with BM25)
162
+ - **Pinecone**: ⚠️ Partial support (requires sparse vectors for true hybrid search)
163
+ - **pgvector**: ✅ Full support (combines vector similarity + PostgreSQL full-text search)
164
+
165
+ **Note for pgvector:** Your table needs a text column with a tsvector index:
166
+ ```sql
167
+ CREATE INDEX idx_content_fts ON my_index USING gin(to_tsvector('english', content));
168
+ ```
169
+
124
170
  ### Dimension Validation
125
171
 
126
172
  Vectra automatically validates that all vectors in a batch have the same dimension:
@@ -136,6 +182,21 @@ client.upsert(vectors: vectors)
136
182
  # => ValidationError: Inconsistent vector dimensions at index 1: expected 3, got 2
137
183
  ```
138
184
 
185
+ ## Rails Generator (vectra:index)
186
+
187
+ For Rails apps, you can generate everything you need for a model with a single command:
188
+
189
+ ```bash
190
+ rails generate vectra:index Product embedding dimension:1536 provider:qdrant
191
+ ```
192
+
193
+ This will:
194
+
195
+ - Create a **pgvector migration** when `provider=pgvector` (adds `embedding` vector column)
196
+ - Generate a **model concern** (`ProductVector`) with `has_vector :embedding`
197
+ - Update the **model** to include `ProductVector`
198
+ - Append an entry to **`config/vectra.yml`** with index metadata (no API keys)
199
+
139
200
  ## Configuration
140
201
 
141
202
  Create a configuration file (Rails: `config/initializers/vectra.rb`):
@@ -26,13 +26,47 @@ vectors = 10_000.times.map { |i| { id: "vec_#{i}", values: Array.new(384) { rand
26
26
  result = batch.upsert_async(
27
27
  index: 'my-index',
28
28
  vectors: vectors,
29
- chunk_size: 100
29
+ chunk_size: 100,
30
+ on_progress: proc { |stats|
31
+ progress = stats[:percentage]
32
+ processed = stats[:processed]
33
+ total = stats[:total]
34
+ chunk = stats[:current_chunk] + 1
35
+ total_chunks = stats[:total_chunks]
36
+
37
+ puts "Progress: #{progress}% (#{processed}/#{total})"
38
+ puts " Chunk #{chunk}/#{total_chunks} | Success: #{stats[:success_count]}, Failed: #{stats[:failed_count]}"
39
+ }
30
40
  )
31
41
 
32
42
  puts "Upserted: #{result[:upserted_count]} vectors in #{result[:chunks]} chunks"
33
43
  puts "Errors: #{result[:errors].size}" if result[:errors].any?
34
44
  ```
35
45
 
46
+ ### Progress Tracking
47
+
48
+ Monitor batch operations in real-time with progress callbacks:
49
+
50
+ ```ruby
51
+ batch.upsert_async(
52
+ index: 'my-index',
53
+ vectors: large_vector_array,
54
+ chunk_size: 100,
55
+ on_progress: proc { |stats|
56
+ # stats contains:
57
+ # - processed: number of processed vectors
58
+ # - total: total number of vectors
59
+ # - percentage: progress percentage (0-100)
60
+ # - current_chunk: current chunk index (0-based)
61
+ # - total_chunks: total number of chunks
62
+ # - success_count: number of successful chunks
63
+ # - failed_count: number of failed chunks
64
+
65
+ puts "Progress: #{stats[:percentage]}% (#{stats[:processed]}/#{stats[:total]})"
66
+ }
67
+ )
68
+ ```
69
+
36
70
  ### Batch Delete
37
71
 
38
72
  ```ruby
data/docs/index.md CHANGED
@@ -34,4 +34,12 @@ results = client.query(
34
34
  results.each do |match|
35
35
  puts "#{match['id']}: #{match['score']}"
36
36
  end
37
+
38
+ # Hybrid search (semantic + keyword)
39
+ results = client.hybrid_search(
40
+ index: 'docs',
41
+ vector: embedding,
42
+ text: 'ruby programming',
43
+ alpha: 0.7 # 70% semantic, 30% keyword
44
+ )
37
45
  ```
@@ -43,6 +43,7 @@ client = Vectra::Client.new(
43
43
  - ✅ ACID transactions
44
44
  - ✅ Complex queries
45
45
  - ✅ Rails ActiveRecord integration
46
+ - ✅ Hybrid search (vector + full-text search)
46
47
 
47
48
  ## Example
48
49
 
@@ -63,6 +64,17 @@ client.upsert(
63
64
 
64
65
  # Search using cosine distance
65
66
  results = client.query(vector: [0.1, 0.2, 0.3], top_k: 5)
67
+
68
+ # Hybrid search (requires text column with tsvector index)
69
+ # First, create the index:
70
+ # CREATE INDEX idx_content_fts ON my_index USING gin(to_tsvector('english', content));
71
+ results = client.hybrid_search(
72
+ index: 'my_index',
73
+ vector: embedding,
74
+ text: 'ruby programming',
75
+ alpha: 0.7,
76
+ text_column: 'content' # default: 'content'
77
+ )
66
78
  ```
67
79
 
68
80
  ## ActiveRecord Integration
@@ -32,6 +32,7 @@ client = Vectra::Client.new(
32
32
  - ✅ Index statistics
33
33
  - ✅ Metadata filtering
34
34
  - ✅ Namespace support
35
+ - ⚠️ Hybrid search (partial - requires sparse vectors)
35
36
 
36
37
  ## Example
37
38
 
@@ -56,6 +57,15 @@ results = client.query(vector: [0.1, 0.2, 0.3], top_k: 5)
56
57
  results.matches.each do |match|
57
58
  puts "#{match['id']}: #{match['score']}"
58
59
  end
60
+
61
+ # Hybrid search (note: requires sparse vectors for true hybrid search)
62
+ # For now, this uses dense vector search only
63
+ results = client.hybrid_search(
64
+ index: 'my-index',
65
+ vector: embedding,
66
+ text: 'ruby programming',
67
+ alpha: 0.7
68
+ )
59
69
  ```
60
70
 
61
71
  ## Configuration Options
@@ -56,6 +56,14 @@ client.upsert(
56
56
 
57
57
  # Search
58
58
  results = client.query(vector: [0.1, 0.2, 0.3], top_k: 10)
59
+
60
+ # Hybrid search (semantic + keyword)
61
+ results = client.hybrid_search(
62
+ index: 'my-collection',
63
+ vector: embedding,
64
+ text: 'ruby programming',
65
+ alpha: 0.7 # 70% semantic, 30% keyword
66
+ )
59
67
  ```
60
68
 
61
69
  ## Configuration Options
@@ -47,6 +47,7 @@ client = Vectra::Client.new(
47
47
  - ✅ Delete by IDs or filter
48
48
  - ✅ List and describe classes
49
49
  - ✅ Basic stats via GraphQL `Aggregate`
50
+ - ✅ Hybrid search (BM25 + vector similarity)
50
51
 
51
52
  ## Basic Example
52
53
 
@@ -89,6 +90,15 @@ results = client.query(
89
90
  results.each do |match|
90
91
  puts "#{match.id} (score=#{match.score.round(3)}): #{match.metadata["title"]}"
91
92
  end
93
+
94
+ # Hybrid search (BM25 + vector)
95
+ results = client.hybrid_search(
96
+ index: index,
97
+ vector: embedding,
98
+ text: 'ruby programming',
99
+ alpha: 0.7, # 70% semantic, 30% keyword
100
+ top_k: 10
101
+ )
92
102
  ```
93
103
 
94
104
  ## Advanced Filtering
data/examples/README.md CHANGED
@@ -29,6 +29,24 @@ psql vectra_demo -c "CREATE EXTENSION vector;"
29
29
 
30
30
  ## Examples
31
31
 
32
+ ### Rails Quickstart (with generator)
33
+
34
+ ```bash
35
+ # Generate Vectra index for Product model
36
+ rails generate vectra:index Product embedding dimension:1536 provider:qdrant
37
+
38
+ # Run migrations (if pgvector)
39
+ rails db:migrate
40
+ ```
41
+
42
+ Then in `app/models/product.rb`:
43
+
44
+ ```ruby
45
+ class Product < ApplicationRecord
46
+ include ProductVector # generated concern with has_vector :embedding
47
+ end
48
+ ```
49
+
32
50
  ### 1. **Comprehensive Demo** (`comprehensive_demo.rb`)
33
51
 
34
52
  **⭐ START HERE** - Complete production-ready demonstration of all Vectra features.