Skip to main content

prax_pgvector/
index.rs

1//! Vector index management for pgvector.
2//!
3//! pgvector supports two approximate nearest-neighbor (ANN) index types:
4//!
5//! | Index | Algorithm | Best For | Tradeoff |
6//! |-------|-----------|----------|----------|
7//! | **IVFFlat** | Inverted file with flat quantization | Large datasets, tunable recall | Requires training data |
8//! | **HNSW** | Hierarchical navigable small world | Most workloads, no training needed | Higher memory usage |
9//!
10//! # Choosing an Index
11//!
12//! - **HNSW** is recommended for most use cases — better recall/speed tradeoff,
13//!   no training step, and supports concurrent inserts.
14//! - **IVFFlat** is useful when memory is constrained or when you have very
15//!   large datasets and can tolerate a training step.
16
17use std::fmt;
18
19use serde::{Deserialize, Serialize};
20
21use crate::error::{VectorError, VectorResult};
22use crate::ops::{BinaryDistanceMetric, DistanceMetric};
23
24/// The type of ANN index to create.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[non_exhaustive]
27pub enum IndexType {
28    /// IVFFlat (Inverted File with Flat quantization).
29    IvfFlat(IvfFlatConfig),
30
31    /// HNSW (Hierarchical Navigable Small World).
32    Hnsw(HnswConfig),
33}
34
35impl fmt::Display for IndexType {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::IvfFlat(_) => write!(f, "ivfflat"),
39            Self::Hnsw(_) => write!(f, "hnsw"),
40        }
41    }
42}
43
44/// Configuration for IVFFlat indexes.
45///
46/// IVFFlat divides vectors into `lists` number of clusters during a training phase.
47/// At query time, `probes` clusters are searched.
48///
49/// # Tuning Guidelines
50///
51/// - `lists`: Start with `rows / 1000` for up to 1M rows, `sqrt(rows)` for more.
52/// - `probes`: Start with `sqrt(lists)` and increase for better recall.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct IvfFlatConfig {
55    /// Number of inverted lists (clusters).
56    ///
57    /// More lists = faster search but potentially lower recall.
58    /// Recommended: `rows / 1000` for up to 1M rows.
59    pub lists: usize,
60}
61
62impl IvfFlatConfig {
63    /// Create a new IVFFlat config with the given number of lists.
64    pub fn new(lists: usize) -> Self {
65        Self { lists }
66    }
67
68    /// Create a config with the recommended number of lists for a given row count.
69    pub fn for_row_count(rows: usize) -> Self {
70        let lists = if rows <= 1_000_000 {
71            (rows / 1000).max(1)
72        } else {
73            (rows as f64).sqrt() as usize
74        };
75        Self { lists }
76    }
77}
78
79impl Default for IvfFlatConfig {
80    fn default() -> Self {
81        Self { lists: 100 }
82    }
83}
84
85/// Configuration for HNSW indexes.
86///
87/// HNSW builds a multi-layered graph that enables efficient approximate nearest-neighbor
88/// search without a separate training step.
89///
90/// # Tuning Guidelines
91///
92/// - `m`: Number of connections per node. Higher = better recall, more memory.
93///   Default: 16. Range: 2-100.
94/// - `ef_construction`: Size of the dynamic candidate list during index build.
95///   Higher = better recall, slower build. Default: 64. Range: 4-1000.
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct HnswConfig {
98    /// Maximum number of connections per node per layer.
99    ///
100    /// Higher values improve recall but increase memory and build time.
101    /// Default: 16.
102    pub m: Option<usize>,
103
104    /// Size of the dynamic candidate list during construction.
105    ///
106    /// Higher values improve index quality but slow down build.
107    /// Default: 64.
108    pub ef_construction: Option<usize>,
109}
110
111impl HnswConfig {
112    /// Create a new HNSW config with defaults.
113    pub fn new() -> Self {
114        Self {
115            m: None,
116            ef_construction: None,
117        }
118    }
119
120    /// Set the `m` parameter (connections per node).
121    pub fn m(mut self, m: usize) -> Self {
122        self.m = Some(m);
123        self
124    }
125
126    /// Set the `ef_construction` parameter.
127    pub fn ef_construction(mut self, ef: usize) -> Self {
128        self.ef_construction = Some(ef);
129        self
130    }
131
132    /// High-recall configuration (slower build, better search quality).
133    pub fn high_recall() -> Self {
134        Self {
135            m: Some(32),
136            ef_construction: Some(128),
137        }
138    }
139
140    /// Fast-build configuration (faster build, lower recall).
141    pub fn fast_build() -> Self {
142        Self {
143            m: Some(8),
144            ef_construction: Some(32),
145        }
146    }
147}
148
149impl Default for HnswConfig {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155/// A vector index definition.
156///
157/// # Examples
158///
159/// ```rust
160/// use prax_pgvector::index::{VectorIndex, HnswConfig};
161/// use prax_pgvector::DistanceMetric;
162///
163/// // Create an HNSW index
164/// let index = VectorIndex::hnsw("idx_embedding", "documents", "embedding")
165///     .metric(DistanceMetric::Cosine)
166///     .config(HnswConfig::high_recall())
167///     .build()
168///     .unwrap();
169///
170/// let sql = index.to_create_sql();
171/// assert!(sql.contains("USING hnsw"));
172/// assert!(sql.contains("vector_cosine_ops"));
173/// ```
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct VectorIndex {
176    /// Index name.
177    pub name: String,
178    /// Table name.
179    pub table: String,
180    /// Column name.
181    pub column: String,
182    /// Distance metric.
183    pub metric: DistanceMetric,
184    /// Index type and configuration.
185    pub index_type: IndexType,
186    /// Whether to create concurrently (non-blocking).
187    pub concurrent: bool,
188    /// Whether to add IF NOT EXISTS clause.
189    pub if_not_exists: bool,
190}
191
192impl VectorIndex {
193    /// Start building an HNSW index.
194    pub fn hnsw(
195        name: impl Into<String>,
196        table: impl Into<String>,
197        column: impl Into<String>,
198    ) -> VectorIndexBuilder {
199        VectorIndexBuilder {
200            name: name.into(),
201            table: table.into(),
202            column: column.into(),
203            metric: DistanceMetric::L2,
204            index_type: IndexType::Hnsw(HnswConfig::default()),
205            concurrent: false,
206            if_not_exists: false,
207        }
208    }
209
210    /// Start building an IVFFlat index.
211    pub fn ivfflat(
212        name: impl Into<String>,
213        table: impl Into<String>,
214        column: impl Into<String>,
215    ) -> VectorIndexBuilder {
216        VectorIndexBuilder {
217            name: name.into(),
218            table: table.into(),
219            column: column.into(),
220            metric: DistanceMetric::L2,
221            index_type: IndexType::IvfFlat(IvfFlatConfig::default()),
222            concurrent: false,
223            if_not_exists: false,
224        }
225    }
226
227    /// Generate the CREATE INDEX SQL statement.
228    pub fn to_create_sql(&self) -> String {
229        let concurrent = if self.concurrent { " CONCURRENTLY" } else { "" };
230        let if_not_exists = if self.if_not_exists {
231            " IF NOT EXISTS"
232        } else {
233            ""
234        };
235
236        let (method, with_clause) = match &self.index_type {
237            IndexType::IvfFlat(config) => {
238                let with = format!(" WITH (lists = {})", config.lists);
239                ("ivfflat", with)
240            }
241            IndexType::Hnsw(config) => {
242                let mut with_parts = Vec::new();
243                if let Some(m) = config.m {
244                    with_parts.push(format!("m = {m}"));
245                }
246                if let Some(ef) = config.ef_construction {
247                    with_parts.push(format!("ef_construction = {ef}"));
248                }
249                let with = if with_parts.is_empty() {
250                    String::new()
251                } else {
252                    format!(" WITH ({})", with_parts.join(", "))
253                };
254                ("hnsw", with)
255            }
256        };
257
258        format!(
259            "CREATE INDEX{}{} {} ON {} USING {} ({} {}){}",
260            concurrent,
261            if_not_exists,
262            self.name,
263            self.table,
264            method,
265            self.column,
266            self.metric.ops_class(),
267            with_clause
268        )
269    }
270
271    /// Generate the DROP INDEX SQL statement.
272    pub fn to_drop_sql(&self) -> String {
273        let concurrent = if self.concurrent { " CONCURRENTLY" } else { "" };
274        format!("DROP INDEX{} IF EXISTS {}", concurrent, self.name)
275    }
276
277    /// Generate SQL to check if this index exists.
278    pub fn to_exists_sql(&self) -> String {
279        let name = self.name.replace('\'', "''");
280        format!("SELECT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = '{name}')")
281    }
282
283    /// Generate SQL to get the index size.
284    pub fn to_size_sql(&self) -> String {
285        let name = self.name.replace('\'', "''");
286        format!("SELECT pg_size_pretty(pg_relation_size('{name}'))")
287    }
288}
289
290/// Builder for [`VectorIndex`].
291#[derive(Debug, Clone)]
292pub struct VectorIndexBuilder {
293    name: String,
294    table: String,
295    column: String,
296    metric: DistanceMetric,
297    index_type: IndexType,
298    concurrent: bool,
299    if_not_exists: bool,
300}
301
302impl VectorIndexBuilder {
303    /// Set the distance metric.
304    pub fn metric(mut self, metric: DistanceMetric) -> Self {
305        self.metric = metric;
306        self
307    }
308
309    /// Set the HNSW configuration (only effective for HNSW indexes).
310    ///
311    /// This config is ignored when the builder was started with
312    /// [`VectorIndex::ivfflat`] — the index type is never changed by this
313    /// method. Use [`ivfflat_config`](Self::ivfflat_config) to configure
314    /// IVFFlat indexes.
315    pub fn config(mut self, config: HnswConfig) -> Self {
316        if matches!(self.index_type, IndexType::Hnsw(_)) {
317            self.index_type = IndexType::Hnsw(config);
318        }
319        self
320    }
321
322    /// Set the IVFFlat configuration (only effective for IVFFlat indexes).
323    pub fn ivfflat_config(mut self, config: IvfFlatConfig) -> Self {
324        self.index_type = IndexType::IvfFlat(config);
325        self
326    }
327
328    /// Create the index concurrently (non-blocking).
329    pub fn concurrent(mut self) -> Self {
330        self.concurrent = true;
331        self
332    }
333
334    /// Add IF NOT EXISTS clause.
335    pub fn if_not_exists(mut self) -> Self {
336        self.if_not_exists = true;
337        self
338    }
339
340    /// Build the index definition.
341    ///
342    /// # Errors
343    ///
344    /// Returns an error if the configuration is invalid, including if the
345    /// index name contains characters outside the identifier set (ASCII
346    /// alphanumeric, `_`, and `.` for schema-qualified names).
347    pub fn build(self) -> VectorResult<VectorIndex> {
348        if self.name.is_empty() {
349            return Err(VectorError::index("index name cannot be empty"));
350        }
351        if !self
352            .name
353            .chars()
354            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
355        {
356            return Err(VectorError::index(
357                "index name contains invalid characters: only ASCII alphanumeric, '_', and '.' are allowed",
358            ));
359        }
360        if self.table.is_empty() {
361            return Err(VectorError::index("table name cannot be empty"));
362        }
363        if self.column.is_empty() {
364            return Err(VectorError::index("column name cannot be empty"));
365        }
366
367        Ok(VectorIndex {
368            name: self.name,
369            table: self.table,
370            column: self.column,
371            metric: self.metric,
372            index_type: self.index_type,
373            concurrent: self.concurrent,
374            if_not_exists: self.if_not_exists,
375        })
376    }
377}
378
379/// A binary vector index definition.
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381pub struct BinaryVectorIndex {
382    /// Index name.
383    pub name: String,
384    /// Table name.
385    pub table: String,
386    /// Column name.
387    pub column: String,
388    /// Distance metric.
389    pub metric: BinaryDistanceMetric,
390    /// HNSW configuration (only HNSW is supported for bit vectors).
391    pub hnsw_config: HnswConfig,
392    /// Whether to create concurrently.
393    pub concurrent: bool,
394}
395
396impl BinaryVectorIndex {
397    /// Create a new binary vector index builder.
398    #[allow(clippy::new_ret_no_self)]
399    pub fn new(
400        name: impl Into<String>,
401        table: impl Into<String>,
402        column: impl Into<String>,
403    ) -> BinaryVectorIndexBuilder {
404        BinaryVectorIndexBuilder {
405            name: name.into(),
406            table: table.into(),
407            column: column.into(),
408            metric: BinaryDistanceMetric::Hamming,
409            hnsw_config: HnswConfig::default(),
410            concurrent: false,
411        }
412    }
413
414    /// Generate the CREATE INDEX SQL.
415    pub fn to_create_sql(&self) -> String {
416        let concurrent = if self.concurrent { " CONCURRENTLY" } else { "" };
417
418        let mut with_parts = Vec::new();
419        if let Some(m) = self.hnsw_config.m {
420            with_parts.push(format!("m = {m}"));
421        }
422        if let Some(ef) = self.hnsw_config.ef_construction {
423            with_parts.push(format!("ef_construction = {ef}"));
424        }
425        let with = if with_parts.is_empty() {
426            String::new()
427        } else {
428            format!(" WITH ({})", with_parts.join(", "))
429        };
430
431        format!(
432            "CREATE INDEX{} {} ON {} USING hnsw ({} {}){}",
433            concurrent,
434            self.name,
435            self.table,
436            self.column,
437            self.metric.ops_class(),
438            with
439        )
440    }
441}
442
443/// Builder for [`BinaryVectorIndex`].
444#[derive(Debug, Clone)]
445pub struct BinaryVectorIndexBuilder {
446    name: String,
447    table: String,
448    column: String,
449    metric: BinaryDistanceMetric,
450    hnsw_config: HnswConfig,
451    concurrent: bool,
452}
453
454impl BinaryVectorIndexBuilder {
455    /// Set the distance metric.
456    pub fn metric(mut self, metric: BinaryDistanceMetric) -> Self {
457        self.metric = metric;
458        self
459    }
460
461    /// Set the HNSW configuration.
462    pub fn config(mut self, config: HnswConfig) -> Self {
463        self.hnsw_config = config;
464        self
465    }
466
467    /// Create the index concurrently.
468    pub fn concurrent(mut self) -> Self {
469        self.concurrent = true;
470        self
471    }
472
473    /// Build the index definition.
474    pub fn build(self) -> VectorResult<BinaryVectorIndex> {
475        if self.name.is_empty() {
476            return Err(VectorError::index("index name cannot be empty"));
477        }
478        Ok(BinaryVectorIndex {
479            name: self.name,
480            table: self.table,
481            column: self.column,
482            metric: self.metric,
483            hnsw_config: self.hnsw_config,
484            concurrent: self.concurrent,
485        })
486    }
487}
488
489/// SQL helpers for pgvector extension management.
490pub mod extension {
491    /// Generate SQL to create the pgvector extension.
492    pub fn create_extension_sql() -> &'static str {
493        "CREATE EXTENSION IF NOT EXISTS vector"
494    }
495
496    /// Generate SQL to create the pgvector extension in a specific schema.
497    pub fn create_extension_in_schema_sql(schema: &str) -> String {
498        format!("CREATE EXTENSION IF NOT EXISTS vector SCHEMA {schema}")
499    }
500
501    /// Generate SQL to drop the pgvector extension.
502    pub fn drop_extension_sql() -> &'static str {
503        "DROP EXTENSION IF EXISTS vector"
504    }
505
506    /// Generate SQL to check if pgvector is installed.
507    pub fn check_extension_sql() -> &'static str {
508        "SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector')"
509    }
510
511    /// Generate SQL to get the installed pgvector version.
512    pub fn version_sql() -> &'static str {
513        "SELECT extversion FROM pg_extension WHERE extname = 'vector'"
514    }
515
516    /// Generate SQL to create a vector column.
517    pub fn add_vector_column_sql(table: &str, column: &str, dimensions: usize) -> String {
518        format!("ALTER TABLE {table} ADD COLUMN {column} vector({dimensions})")
519    }
520
521    /// Generate SQL to create a halfvec column.
522    pub fn add_halfvec_column_sql(table: &str, column: &str, dimensions: usize) -> String {
523        format!("ALTER TABLE {table} ADD COLUMN {column} halfvec({dimensions})")
524    }
525
526    /// Generate SQL to create a sparsevec column.
527    pub fn add_sparsevec_column_sql(table: &str, column: &str, dimensions: usize) -> String {
528        format!("ALTER TABLE {table} ADD COLUMN {column} sparsevec({dimensions})")
529    }
530
531    /// Generate SQL to create a bit column.
532    pub fn add_bit_column_sql(table: &str, column: &str, dimensions: usize) -> String {
533        format!("ALTER TABLE {table} ADD COLUMN {column} bit({dimensions})")
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540
541    #[test]
542    fn test_hnsw_index_create_sql() {
543        let index = VectorIndex::hnsw("idx_embedding", "documents", "embedding")
544            .metric(DistanceMetric::Cosine)
545            .config(HnswConfig::new().m(16).ef_construction(64))
546            .build()
547            .unwrap();
548
549        let sql = index.to_create_sql();
550        assert!(sql.contains("CREATE INDEX"));
551        assert!(sql.contains("idx_embedding"));
552        assert!(sql.contains("documents"));
553        assert!(sql.contains("USING hnsw"));
554        assert!(sql.contains("vector_cosine_ops"));
555        assert!(sql.contains("m = 16"));
556        assert!(sql.contains("ef_construction = 64"));
557    }
558
559    #[test]
560    fn test_ivfflat_builder_ignores_hnsw_config() {
561        let index = VectorIndex::ivfflat("idx_embedding", "documents", "embedding")
562            .config(HnswConfig::high_recall())
563            .build()
564            .unwrap();
565
566        assert!(matches!(index.index_type, IndexType::IvfFlat(_)));
567        let sql = index.to_create_sql();
568        assert!(sql.contains("USING ivfflat"));
569        assert!(!sql.contains("USING hnsw"));
570    }
571
572    #[test]
573    fn test_hnsw_index_default_config() {
574        let index = VectorIndex::hnsw("idx_emb", "docs", "emb").build().unwrap();
575
576        let sql = index.to_create_sql();
577        assert!(sql.contains("USING hnsw"));
578        assert!(sql.contains("vector_l2_ops")); // default metric
579        assert!(!sql.contains("WITH")); // no config = no WITH clause
580    }
581
582    #[test]
583    fn test_ivfflat_index_create_sql() {
584        let index = VectorIndex::ivfflat("idx_embedding", "documents", "embedding")
585            .metric(DistanceMetric::L2)
586            .ivfflat_config(IvfFlatConfig::new(200))
587            .build()
588            .unwrap();
589
590        let sql = index.to_create_sql();
591        assert!(sql.contains("USING ivfflat"));
592        assert!(sql.contains("vector_l2_ops"));
593        assert!(sql.contains("lists = 200"));
594    }
595
596    #[test]
597    fn test_ivfflat_for_row_count() {
598        let config = IvfFlatConfig::for_row_count(500_000);
599        assert_eq!(config.lists, 500);
600
601        let config = IvfFlatConfig::for_row_count(5_000_000);
602        assert_eq!(config.lists, 2236); // sqrt(5M)
603    }
604
605    #[test]
606    fn test_concurrent_index() {
607        let index = VectorIndex::hnsw("idx_emb", "docs", "emb")
608            .concurrent()
609            .if_not_exists()
610            .build()
611            .unwrap();
612
613        let sql = index.to_create_sql();
614        assert!(sql.contains("CONCURRENTLY"));
615        assert!(sql.contains("IF NOT EXISTS"));
616    }
617
618    #[test]
619    fn test_drop_index() {
620        let index = VectorIndex::hnsw("idx_emb", "docs", "emb").build().unwrap();
621
622        let sql = index.to_drop_sql();
623        assert_eq!(sql, "DROP INDEX IF EXISTS idx_emb");
624    }
625
626    #[test]
627    fn test_concurrent_drop_index() {
628        let index = VectorIndex::hnsw("idx_emb", "docs", "emb")
629            .concurrent()
630            .build()
631            .unwrap();
632
633        let sql = index.to_drop_sql();
634        assert!(sql.contains("CONCURRENTLY"));
635    }
636
637    #[test]
638    fn test_index_exists_sql() {
639        let index = VectorIndex::hnsw("idx_emb", "docs", "emb").build().unwrap();
640
641        let sql = index.to_exists_sql();
642        assert!(sql.contains("pg_indexes"));
643        assert!(sql.contains("idx_emb"));
644    }
645
646    #[test]
647    fn test_index_size_sql() {
648        let index = VectorIndex::hnsw("idx_emb", "docs", "emb").build().unwrap();
649
650        let sql = index.to_size_sql();
651        assert!(sql.contains("pg_size_pretty"));
652        assert!(sql.contains("idx_emb"));
653    }
654
655    #[test]
656    fn test_index_name_with_quote_rejected_at_build() {
657        let result = VectorIndex::hnsw("idx'; DROP TABLE users; --", "docs", "emb").build();
658        assert!(result.is_err());
659    }
660
661    #[test]
662    fn test_schema_qualified_index_name_allowed() {
663        let index = VectorIndex::hnsw("myschema.idx_emb", "docs", "emb")
664            .build()
665            .unwrap();
666
667        let sql = index.to_exists_sql();
668        assert!(sql.contains("myschema.idx_emb"));
669    }
670
671    #[test]
672    fn test_exists_and_size_sql_escape_quotes() {
673        // VectorIndex fields are public, so a name can bypass build()
674        // validation; the SQL generators still escape quotes defensively.
675        let mut index = VectorIndex::hnsw("idx_emb", "docs", "emb").build().unwrap();
676        index.name = "idx'evil".to_string();
677
678        assert!(index.to_exists_sql().contains("idx''evil"));
679        assert!(index.to_size_sql().contains("idx''evil"));
680    }
681
682    #[test]
683    fn test_empty_name_error() {
684        let result = VectorIndex::hnsw("", "docs", "emb").build();
685        assert!(result.is_err());
686    }
687
688    #[test]
689    fn test_hnsw_high_recall() {
690        let config = HnswConfig::high_recall();
691        assert_eq!(config.m, Some(32));
692        assert_eq!(config.ef_construction, Some(128));
693    }
694
695    #[test]
696    fn test_hnsw_fast_build() {
697        let config = HnswConfig::fast_build();
698        assert_eq!(config.m, Some(8));
699        assert_eq!(config.ef_construction, Some(32));
700    }
701
702    #[test]
703    fn test_binary_vector_index() {
704        let index = BinaryVectorIndex::new("idx_bits", "docs", "binary_emb")
705            .metric(BinaryDistanceMetric::Hamming)
706            .build()
707            .unwrap();
708
709        let sql = index.to_create_sql();
710        assert!(sql.contains("USING hnsw"));
711        assert!(sql.contains("bit_hamming_ops"));
712    }
713
714    #[test]
715    fn test_extension_create_sql() {
716        assert_eq!(
717            extension::create_extension_sql(),
718            "CREATE EXTENSION IF NOT EXISTS vector"
719        );
720    }
721
722    #[test]
723    fn test_extension_in_schema() {
724        let sql = extension::create_extension_in_schema_sql("public");
725        assert!(sql.contains("SCHEMA public"));
726    }
727
728    #[test]
729    fn test_add_vector_column() {
730        let sql = extension::add_vector_column_sql("documents", "embedding", 1536);
731        assert_eq!(
732            sql,
733            "ALTER TABLE documents ADD COLUMN embedding vector(1536)"
734        );
735    }
736
737    #[test]
738    fn test_add_sparsevec_column() {
739        let sql = extension::add_sparsevec_column_sql("documents", "sparse_emb", 30000);
740        assert!(sql.contains("sparsevec(30000)"));
741    }
742
743    #[test]
744    fn test_add_bit_column() {
745        let sql = extension::add_bit_column_sql("documents", "binary_emb", 1024);
746        assert!(sql.contains("bit(1024)"));
747    }
748
749    #[test]
750    fn test_check_extension_sql() {
751        let sql = extension::check_extension_sql();
752        assert!(sql.contains("pg_extension"));
753    }
754
755    #[test]
756    fn test_version_sql() {
757        let sql = extension::version_sql();
758        assert!(sql.contains("extversion"));
759    }
760
761    #[test]
762    fn test_index_type_display() {
763        let ivf = IndexType::IvfFlat(IvfFlatConfig::default());
764        assert_eq!(format!("{ivf}"), "ivfflat");
765
766        let hnsw = IndexType::Hnsw(HnswConfig::default());
767        assert_eq!(format!("{hnsw}"), "hnsw");
768    }
769
770    #[test]
771    fn test_all_metrics_with_ivfflat() {
772        for metric in [
773            DistanceMetric::L2,
774            DistanceMetric::InnerProduct,
775            DistanceMetric::Cosine,
776            DistanceMetric::L1,
777        ] {
778            let index = VectorIndex::ivfflat("idx", "t", "c")
779                .metric(metric)
780                .build()
781                .unwrap();
782            let sql = index.to_create_sql();
783            assert!(sql.contains(metric.ops_class()));
784        }
785    }
786
787    #[test]
788    fn test_all_metrics_with_hnsw() {
789        for metric in [
790            DistanceMetric::L2,
791            DistanceMetric::InnerProduct,
792            DistanceMetric::Cosine,
793            DistanceMetric::L1,
794        ] {
795            let index = VectorIndex::hnsw("idx", "t", "c")
796                .metric(metric)
797                .build()
798                .unwrap();
799            let sql = index.to_create_sql();
800            assert!(sql.contains(metric.ops_class()));
801        }
802    }
803}