Skip to main content

hermes_core/index/
vector_builder.rs

1//! Vector index building for IndexWriter
2//!
3//! Training is **manual-only** — decoupled from commit.
4//! Call `build_vector_index()` explicitly when ready.
5//! ANN indexes are built naturally during subsequent merges.
6
7use std::io::Write;
8use std::sync::Arc;
9
10use rustc_hash::FxHashMap;
11
12use crate::directories::DirectoryWriter;
13use crate::dsl::{DenseVectorConfig, Field, FieldType, VectorIndexType};
14use crate::error::{Error, Result};
15use crate::segment::{SegmentId, SegmentReader};
16
17use super::IndexWriter;
18
19/// Maximum supported IVF centroid count. Query-side `nprobe` and serialized
20/// cluster identifiers use the same practical bound.
21const MAX_IVF_CLUSTERS: usize = 4096;
22
23struct TrainedFieldUpdate {
24    field_id: u32,
25    index_type: VectorIndexType,
26    vector_count: usize,
27    num_clusters: usize,
28    centroids_file: String,
29    codebook_file: Option<String>,
30}
31
32/// Write adapter that rejects an artifact before its serialized form exceeds
33/// the same bound enforced by the loader. Encoding directly through this
34/// adapter avoids materializing a second, potentially hundreds-of-megabytes
35/// copy of the trained structure.
36struct SizeLimitedWriter<'a, W: Write + ?Sized> {
37    inner: &'a mut W,
38    written: usize,
39    limit: usize,
40}
41
42impl<'a, W: Write + ?Sized> SizeLimitedWriter<'a, W> {
43    fn new(inner: &'a mut W, limit: usize) -> Self {
44        Self {
45            inner,
46            written: 0,
47            limit,
48        }
49    }
50}
51
52impl<W: Write + ?Sized> Write for SizeLimitedWriter<'_, W> {
53    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
54        let next_size = self
55            .written
56            .checked_add(buffer.len())
57            .ok_or_else(|| std::io::Error::other("trained artifact size overflow"))?;
58        if next_size > self.limit {
59            return Err(std::io::Error::new(
60                std::io::ErrorKind::InvalidData,
61                format!(
62                    "trained artifact exceeds the {}-byte safety limit",
63                    self.limit
64                ),
65            ));
66        }
67        let written = self.inner.write(buffer)?;
68        self.written += written;
69        Ok(written)
70    }
71
72    fn flush(&mut self) -> std::io::Result<()> {
73        self.inner.flush()
74    }
75}
76
77fn validate_explicit_ivf_num_clusters(config: &DenseVectorConfig) -> Result<()> {
78    match config.num_clusters {
79        Some(0) => Err(Error::Schema(
80            "dense vector num_clusters must be at least 1".to_string(),
81        )),
82        Some(value) if value > MAX_IVF_CLUSTERS => Err(Error::Schema(format!(
83            "dense vector num_clusters must not exceed {MAX_IVF_CLUSTERS}, got {value}"
84        ))),
85        _ => Ok(()),
86    }
87}
88
89/// Validate the configured centroid count and cap it to the training sample.
90///
91/// Corpus size drives the automatic heuristic, but training cannot produce
92/// more distinct centroids than the number of sampled vectors. Keeping this
93/// decision here avoids relying on a panic-prone, implicit clamp inside the
94/// trainer and gives callers a schema error for invalid explicit values.
95fn effective_ivf_num_clusters(
96    config: &DenseVectorConfig,
97    corpus_count: usize,
98    sample_count: usize,
99) -> Result<usize> {
100    if sample_count == 0 {
101        return Err(Error::Schema(
102            "cannot train an IVF vector index without sample vectors".to_string(),
103        ));
104    }
105
106    validate_explicit_ivf_num_clusters(config)?;
107    let requested = match config.num_clusters {
108        Some(value) => value,
109        None => config.optimal_num_clusters(corpus_count),
110    };
111
112    Ok(requested.min(sample_count))
113}
114
115impl<D: DirectoryWriter + 'static> IndexWriter<D> {
116    /// Train vector index from accumulated Flat vectors (manual, not auto-triggered).
117    ///
118    /// 1. Acquires a snapshot (segments safe to read)
119    /// 2. Collects vectors for training
120    /// 3. Trains centroids/codebooks
121    /// 4. Updates metadata (marks fields as Built)
122    /// 5. Publishes to ArcSwap — merges will use these automatically
123    ///
124    /// Existing flat segments get ANN during normal merges. No rebuild needed.
125    pub async fn build_vector_index(&self) -> Result<()> {
126        let dense_fields = self.get_dense_vector_fields();
127        if dense_fields.is_empty() {
128            log::info!("No dense vector fields configured for ANN indexing");
129            return Ok(());
130        }
131
132        let artifact_update = self.segment_manager.begin_vector_artifact_update().await?;
133        self.build_vector_index_locked(&dense_fields, &artifact_update)
134            .await
135    }
136
137    /// Build while the SegmentManager's artifact-update gate is held.
138    async fn build_vector_index_locked(
139        &self,
140        dense_fields: &[(Field, DenseVectorConfig)],
141        artifact_update: &crate::merge::VectorArtifactUpdateGuard,
142    ) -> Result<()> {
143        // Check which fields need building (skip already built)
144        let fields_to_build = self.get_fields_to_build(dense_fields).await;
145        if fields_to_build.is_empty() {
146            log::info!("All vector fields already built, skipping training");
147            return Ok(());
148        }
149
150        // Reject malformed explicit settings before opening segments or
151        // allocating the bounded training samples.
152        for (_, config) in &fields_to_build {
153            if config.uses_ivf() {
154                validate_explicit_ivf_num_clusters(config)?;
155            }
156        }
157
158        // Acquire snapshot — segments won't be deleted while we read them
159        let snapshot = self.segment_manager.acquire_snapshot().await;
160        let segment_ids = snapshot.segment_ids();
161        if segment_ids.is_empty() {
162            return Ok(());
163        }
164
165        // Collect vectors for training
166        let (all_vectors, total_vectors) = self
167            .collect_vectors_for_training(segment_ids, &fields_to_build)
168            .await?;
169
170        // Train every requested field before changing metadata. If any field
171        // fails, all durable field states remain Flat and the successfully
172        // written files are merely unreferenced retry targets.
173        let mut updates = Vec::with_capacity(fields_to_build.len());
174        for (field, config) in &fields_to_build {
175            if let Some(update) = self
176                .train_field_index(*field, config, &all_vectors, &total_vectors)
177                .await?
178            {
179                updates.push(update);
180            }
181        }
182
183        if updates.is_empty() {
184            log::info!("No vectors available for trained vector-index fields");
185            return Ok(());
186        }
187
188        // Durable metadata and the complete validated ArcSwap set advance in a
189        // single cancellation-safe SegmentManager transaction.
190        self.segment_manager
191            .update_vector_metadata_and_publish(artifact_update, |meta| {
192                for update in &updates {
193                    meta.init_field(update.field_id, update.index_type);
194                    meta.mark_field_built(
195                        update.field_id,
196                        update.vector_count,
197                        update.num_clusters,
198                        update.centroids_file.clone(),
199                        update.codebook_file.clone(),
200                    );
201                }
202            })
203            .await?;
204
205        log::info!("Vector index training complete, ANN will be built during merges");
206
207        Ok(())
208    }
209
210    /// Rebuild vector index by retraining centroids/codebooks.
211    ///
212    /// Rebuilding a global artifact generation is only safe while every
213    /// committed segment is still flat. IVF/ScaNN segments embed the artifact
214    /// versions they were built with and cannot be interpreted by freshly
215    /// trained centroids/codebooks.
216    pub async fn rebuild_vector_index(&self) -> Result<()> {
217        let dense_fields = self.get_dense_vector_fields();
218        if dense_fields.is_empty() {
219            return Ok(());
220        }
221
222        // Raise the producer gate and drain operations that may already have
223        // captured the previous trained generation. New producers continue in
224        // flat mode until this guard drops.
225        let artifact_update = self.segment_manager.begin_vector_artifact_update().await?;
226        let snapshot = self.segment_manager.acquire_snapshot().await;
227        let field_ids: Vec<u32> = dense_fields.iter().map(|(field, _)| field.0).collect();
228        self.reject_rebuild_with_ann_segments(snapshot.segment_ids(), &field_ids)
229            .await?;
230
231        // Reset metadata and the ArcSwap set together. Old fixed-name artifact
232        // files are left in place until the atomic writer replaces them; this
233        // avoids a cancellation window and does not accumulate generations.
234        self.segment_manager
235            .update_vector_metadata_and_publish(&artifact_update, |meta| {
236                for field_id in &field_ids {
237                    if let Some(field_meta) = meta.vector_fields.get_mut(field_id) {
238                        field_meta.state = super::VectorIndexState::Flat;
239                        field_meta.centroids_file = None;
240                        field_meta.codebook_file = None;
241                    }
242                }
243                meta.refresh_total_vectors();
244            })
245            .await?;
246
247        log::info!("Reset vector index state to Flat, triggering rebuild...");
248
249        self.build_vector_index_locked(&dense_fields, &artifact_update)
250            .await
251    }
252
253    // ========================================================================
254    // Helper methods
255    // ========================================================================
256
257    async fn reject_rebuild_with_ann_segments(
258        &self,
259        segment_ids: &[String],
260        field_ids: &[u32],
261    ) -> Result<()> {
262        for id_str in segment_ids {
263            let segment_id = SegmentId::from_hex(id_str)
264                .ok_or_else(|| Error::Corruption(format!("Invalid segment ID: {id_str}")))?;
265            let reader = SegmentReader::open_with_cache_blocks(
266                self.directory.as_ref(),
267                segment_id,
268                Arc::clone(&self.schema),
269                self.config.term_cache_blocks,
270                self.config.store_cache_blocks,
271            )
272            .await?;
273            Self::reject_ann_in_reader(&reader, id_str, field_ids)?;
274        }
275        Ok(())
276    }
277
278    fn reject_ann_in_reader(reader: &SegmentReader, id_str: &str, field_ids: &[u32]) -> Result<()> {
279        for &field_id in field_ids {
280            if matches!(
281                reader.vector_indexes().get(&field_id),
282                Some(crate::segment::VectorIndex::IVF(_))
283                    | Some(crate::segment::VectorIndex::ScaNN(_))
284            ) {
285                return Err(Error::Schema(format!(
286                    "cannot retrain vector artifacts for field {field_id}: segment {id_str} \
287                     already contains an IVF/ScaNN index built with the current generation; \
288                     rebuild requires all committed segments for the field to be flat"
289                )));
290            }
291        }
292        Ok(())
293    }
294
295    /// Get all dense vector fields that need ANN indexes
296    fn get_dense_vector_fields(&self) -> Vec<(Field, DenseVectorConfig)> {
297        self.schema
298            .fields()
299            .filter_map(|(field, entry)| {
300                if entry.field_type == FieldType::DenseVector && entry.indexed {
301                    entry
302                        .dense_vector_config
303                        .as_ref()
304                        // Only IVF-backed indexes require a global training
305                        // artifact. Standalone RaBitQ trains per segment; including
306                        // it here repeatedly sampled up to 100k vectors and then
307                        // returned without producing metadata.
308                        .filter(|c| c.uses_ivf())
309                        .map(|c| (field, c.clone()))
310                } else {
311                    None
312                }
313            })
314            .collect()
315    }
316
317    /// Get fields that need building (not already built)
318    async fn get_fields_to_build(
319        &self,
320        dense_fields: &[(Field, DenseVectorConfig)],
321    ) -> Vec<(Field, DenseVectorConfig)> {
322        let field_ids: Vec<u32> = dense_fields.iter().map(|(f, _)| f.0).collect();
323        let built: Vec<u32> = self
324            .segment_manager
325            .read_metadata(|meta| {
326                field_ids
327                    .iter()
328                    .filter(|fid| meta.is_field_built(**fid))
329                    .copied()
330                    .collect()
331            })
332            .await;
333        dense_fields
334            .iter()
335            .filter(|(field, _)| !built.contains(&field.0))
336            .cloned()
337            .collect()
338    }
339
340    /// Collect vectors from segments for training, with sampling for large datasets.
341    ///
342    /// K-means clustering converges well with ~100K samples, so we cap collection
343    /// per field to avoid loading millions of vectors into memory.
344    async fn collect_vectors_for_training(
345        &self,
346        segment_ids: &[String],
347        fields_to_build: &[(Field, DenseVectorConfig)],
348    ) -> Result<(FxHashMap<u32, Vec<Vec<f32>>>, FxHashMap<u32, usize>)> {
349        /// Maximum vectors per field for training. K-means converges well with ~100K samples.
350        const MAX_TRAINING_VECTORS: usize = 100_000;
351
352        let mut all_vectors: FxHashMap<u32, Vec<Vec<f32>>> = FxHashMap::default();
353        let mut total_vectors: FxHashMap<u32, usize> = FxHashMap::default();
354        let mut total_skipped = 0usize;
355        let field_ids: Vec<u32> = fields_to_build.iter().map(|(field, _)| field.0).collect();
356
357        for id_str in segment_ids {
358            let segment_id = SegmentId::from_hex(id_str)
359                .ok_or_else(|| Error::Corruption(format!("Invalid segment ID: {}", id_str)))?;
360            let reader = SegmentReader::open_with_cache_blocks(
361                self.directory.as_ref(),
362                segment_id,
363                Arc::clone(&self.schema),
364                self.config.term_cache_blocks,
365                self.config.store_cache_blocks,
366            )
367            .await?;
368
369            // `build_vector_index` is also effectively a retrain whenever
370            // metadata says Flat. A crash-interrupted rebuild from an older
371            // Hermes version can leave that state beside committed ANN
372            // segments, so validate generation safety during the same segment
373            // scan that collects the samples.
374            Self::reject_ann_in_reader(&reader, id_str, &field_ids)?;
375
376            for (field_id, lazy_flat) in reader.flat_vectors() {
377                if !fields_to_build.iter().any(|(f, _)| f.0 == *field_id) {
378                    continue;
379                }
380                let total = total_vectors.entry(*field_id).or_default();
381                *total = total.saturating_add(lazy_flat.num_vectors);
382                let entry = all_vectors.entry(*field_id).or_default();
383                let remaining = MAX_TRAINING_VECTORS.saturating_sub(entry.len());
384
385                if remaining == 0 {
386                    total_skipped += lazy_flat.num_vectors;
387                    continue;
388                }
389
390                let n = lazy_flat.num_vectors;
391                let dim = lazy_flat.dim;
392                let quant = lazy_flat.quantization;
393
394                // Determine which vector indices to collect
395                let indices: Vec<usize> = if n <= remaining {
396                    (0..n).collect()
397                } else {
398                    let step = (n / remaining).max(1);
399                    (0..n).step_by(step).take(remaining).collect()
400                };
401
402                if indices.len() < n {
403                    total_skipped += n - indices.len();
404                }
405
406                // Batch-read and dequantize instead of one-by-one get_vector()
407                const BATCH: usize = 1024;
408                let mut f32_buf = vec![0f32; BATCH * dim];
409                for chunk in indices.chunks(BATCH) {
410                    // For contiguous ranges, use batch read
411                    let start = chunk[0];
412                    let end = *chunk.last().unwrap();
413                    if end - start + 1 == chunk.len() {
414                        // Contiguous — single batch read
415                        if let Ok(batch_bytes) =
416                            lazy_flat.read_vectors_batch(start, chunk.len()).await
417                        {
418                            let floats = chunk.len() * dim;
419                            f32_buf.resize(floats, 0.0);
420                            crate::segment::dequantize_raw(
421                                batch_bytes.as_slice(),
422                                quant,
423                                floats,
424                                &mut f32_buf,
425                            )
426                            .map_err(crate::Error::Io)?;
427                            for i in 0..chunk.len() {
428                                entry.push(f32_buf[i * dim..(i + 1) * dim].to_vec());
429                            }
430                        }
431                    } else {
432                        // Non-contiguous (sampled) — read individually but reuse buffer
433                        f32_buf.resize(dim, 0.0);
434                        for &idx in chunk {
435                            if let Ok(()) = lazy_flat.read_vector_into(idx, &mut f32_buf).await {
436                                entry.push(f32_buf[..dim].to_vec());
437                            }
438                        }
439                    }
440                }
441            }
442        }
443
444        if total_skipped > 0 {
445            let collected: usize = all_vectors.values().map(|v| v.len()).sum();
446            log::info!(
447                "Sampled {} vectors for training (skipped {}, max {} per field)",
448                collected,
449                total_skipped,
450                MAX_TRAINING_VECTORS,
451            );
452        }
453
454        Ok((all_vectors, total_vectors))
455    }
456
457    /// Train index for a single field
458    async fn train_field_index(
459        &self,
460        field: Field,
461        config: &DenseVectorConfig,
462        all_vectors: &FxHashMap<u32, Vec<Vec<f32>>>,
463        total_vectors: &FxHashMap<u32, usize>,
464    ) -> Result<Option<TrainedFieldUpdate>> {
465        let field_id = field.0;
466        let vectors = match all_vectors.get(&field_id) {
467            Some(v) if !v.is_empty() => v,
468            _ => return Ok(None),
469        };
470
471        let dim = config.dim;
472        let sample_count = vectors.len();
473        let corpus_count = total_vectors
474            .get(&field_id)
475            .copied()
476            .unwrap_or(sample_count);
477        // RaBitQ is trained independently per segment and does not need an
478        // index-level centroid artifact.
479        if !matches!(
480            config.index_type,
481            VectorIndexType::IvfRaBitQ | VectorIndexType::ScaNN
482        ) {
483            return Ok(None);
484        }
485
486        let num_clusters = effective_ivf_num_clusters(config, corpus_count, sample_count)?;
487
488        log::info!(
489            "Training vector index for field {} with {} sampled / {} total vectors, {} clusters (dim={})",
490            field_id,
491            sample_count,
492            corpus_count,
493            num_clusters,
494            dim,
495        );
496
497        let centroids_filename = format!("field_{}_centroids.bin", field_id);
498        let mut codebook_filename: Option<String> = None;
499
500        let actual_num_clusters = match config.index_type {
501            VectorIndexType::IvfRaBitQ => {
502                self.train_ivf_rabitq(
503                    field_id,
504                    dim,
505                    num_clusters,
506                    config.soar.clone(),
507                    vectors,
508                    &centroids_filename,
509                )
510                .await?
511            }
512            VectorIndexType::ScaNN => {
513                codebook_filename = Some(format!("field_{}_codebook.bin", field_id));
514                self.train_scann(
515                    field_id,
516                    dim,
517                    num_clusters,
518                    config.soar.clone(),
519                    vectors,
520                    &centroids_filename,
521                    codebook_filename.as_ref().unwrap(),
522                )
523                .await?
524            }
525            _ => unreachable!("non-IVF vector index returned above"),
526        };
527
528        Ok(Some(TrainedFieldUpdate {
529            field_id,
530            index_type: config.index_type,
531            vector_count: corpus_count,
532            num_clusters: actual_num_clusters,
533            centroids_file: centroids_filename,
534            codebook_file: codebook_filename,
535        }))
536    }
537
538    /// Serialize a trained structure to bincode and save to an index-level file.
539    async fn save_trained_artifact(
540        &self,
541        artifact: &impl serde::Serialize,
542        filename: &str,
543    ) -> Result<()> {
544        let temp_filename = format!("{filename}.tmp");
545        let temp_path = std::path::Path::new(&temp_filename);
546        let final_path = std::path::Path::new(filename);
547        let mut writer = self.directory.streaming_writer(temp_path).await?;
548        let encode_result = {
549            let mut limited = SizeLimitedWriter::new(
550                writer.as_mut(),
551                super::metadata::MAX_TRAINED_ARTIFACT_BYTES,
552            );
553            bincode::serde::encode_into_std_write(
554                artifact,
555                &mut limited,
556                bincode::config::standard(),
557            )
558        };
559        if let Err(error) = encode_result {
560            drop(writer);
561            let _ = self.directory.delete(temp_path).await;
562            return Err(Error::Serialization(format!(
563                "failed to serialize trained artifact '{filename}': {error}"
564            )));
565        }
566        if let Err(error) = writer.finish() {
567            let _ = self.directory.delete(temp_path).await;
568            return Err(Error::Io(error));
569        }
570        if let Err(error) = self.directory.rename(temp_path, final_path).await {
571            let _ = self.directory.delete(temp_path).await;
572            return Err(Error::Io(error));
573        }
574        self.directory.sync().await?;
575        Ok(())
576    }
577
578    /// Train IVF-RaBitQ centroids
579    async fn train_ivf_rabitq(
580        &self,
581        field_id: u32,
582        dim: usize,
583        num_clusters: usize,
584        soar: Option<crate::structures::SoarConfig>,
585        vectors: &[Vec<f32>],
586        centroids_filename: &str,
587    ) -> Result<usize> {
588        let mut coarse_config = crate::structures::CoarseConfig::new(dim, num_clusters);
589        if let Some(soar) = soar {
590            coarse_config = coarse_config.with_soar(soar);
591        }
592        let centroids = crate::structures::CoarseCentroids::train(&coarse_config, vectors);
593        self.save_trained_artifact(&centroids, centroids_filename)
594            .await?;
595
596        log::info!(
597            "Saved IVF-RaBitQ centroids for field {} ({} clusters, soar={})",
598            field_id,
599            centroids.num_clusters,
600            centroids.soar_config.is_some()
601        );
602        Ok(centroids.num_clusters as usize)
603    }
604
605    /// Train ScaNN (IVF-PQ) centroids and codebook
606    #[allow(clippy::too_many_arguments)]
607    async fn train_scann(
608        &self,
609        field_id: u32,
610        dim: usize,
611        num_clusters: usize,
612        soar: Option<crate::structures::SoarConfig>,
613        vectors: &[Vec<f32>],
614        centroids_filename: &str,
615        codebook_filename: &str,
616    ) -> Result<usize> {
617        let mut coarse_config = crate::structures::CoarseConfig::new(dim, num_clusters);
618        if let Some(soar) = soar {
619            coarse_config = coarse_config.with_soar(soar);
620        }
621        let centroids = crate::structures::CoarseCentroids::train(&coarse_config, vectors);
622        self.save_trained_artifact(&centroids, centroids_filename)
623            .await?;
624
625        let pq_config = crate::structures::PQConfig::new(dim);
626        let codebook = crate::structures::PQCodebook::train(pq_config, vectors, 10);
627        self.save_trained_artifact(&codebook, codebook_filename)
628            .await?;
629
630        log::info!(
631            "Saved ScaNN centroids and codebook for field {} ({} clusters)",
632            field_id,
633            centroids.num_clusters
634        );
635        Ok(centroids.num_clusters as usize)
636    }
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642
643    fn ivf_config(num_clusters: Option<usize>) -> DenseVectorConfig {
644        DenseVectorConfig::with_ivf(8, num_clusters, 4)
645    }
646
647    #[test]
648    fn effective_clusters_follow_corpus_heuristic_but_fit_sample() {
649        let config = ivf_config(None);
650
651        assert_eq!(
652            effective_ivf_num_clusters(&config, 1_000_000, 73).unwrap(),
653            73
654        );
655        assert_eq!(
656            effective_ivf_num_clusters(&config, 10_000, 1_000).unwrap(),
657            100
658        );
659    }
660
661    #[test]
662    fn effective_clusters_clamp_explicit_value_to_sample() {
663        let config = ivf_config(Some(256));
664        assert_eq!(
665            effective_ivf_num_clusters(&config, 1_000_000, 17).unwrap(),
666            17
667        );
668    }
669
670    #[test]
671    fn effective_clusters_reject_invalid_explicit_bounds() {
672        let zero = effective_ivf_num_clusters(&ivf_config(Some(0)), 10_000, 100)
673            .unwrap_err()
674            .to_string();
675        assert!(zero.contains("at least 1"));
676
677        let too_many =
678            effective_ivf_num_clusters(&ivf_config(Some(MAX_IVF_CLUSTERS + 1)), 10_000, 100)
679                .unwrap_err()
680                .to_string();
681        assert!(too_many.contains("must not exceed 4096"));
682    }
683
684    #[test]
685    fn effective_clusters_reject_empty_training_sample() {
686        let error = effective_ivf_num_clusters(&ivf_config(None), 10_000, 0)
687            .unwrap_err()
688            .to_string();
689        assert!(error.contains("without sample vectors"));
690    }
691
692    #[test]
693    fn artifact_writer_enforces_limit_without_writing_past_it() {
694        let mut output = Vec::new();
695        let mut writer = SizeLimitedWriter::new(&mut output, 3);
696        writer.write_all(&[1, 2]).unwrap();
697        let error = writer.write_all(&[3, 4]).unwrap_err().to_string();
698        assert!(error.contains("3-byte safety limit"), "{error}");
699        assert_eq!(output, vec![1, 2]);
700    }
701}