use semlith::Semlith;
use std::io::Cursor;
use std::path::Path;
fn shape(dir: &Path, name: &str, colour: [u8; 3], kind: Shape) {
let (w, h) = (224u32, 224u32);
let mut img = image::RgbImage::from_pixel(w, h, image::Rgb([255, 255, 255]));
for y in 0..h {
for x in 0..w {
let inside = match kind {
Shape::Circle => {
let (dx, dy) = (x as f32 - 112.0, y as f32 - 112.0);
dx * dx + dy * dy < 90.0 * 90.0
}
Shape::Square => (30..194).contains(&x) && (30..194).contains(&y),
Shape::Triangle => {
let t = y as f32 / h as f32;
let half = t * 100.0;
(x as f32 - 112.0).abs() < half && y > 30
}
};
if inside {
img.put_pixel(x, y, image::Rgb(colour));
}
}
}
let path = dir.join(name);
image::DynamicImage::ImageRgb8(img)
.save(&path)
.unwrap_or_else(|e| panic!("writing {}: {e}", path.display()));
}
enum Shape {
Circle,
Square,
Triangle,
}
fn corpus() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
shape(dir.path(), "red-circle.png", [220, 20, 20], Shape::Circle);
shape(dir.path(), "blue-square.jpg", [20, 40, 220], Shape::Square);
shape(
dir.path(),
"green-triangle.webp",
[20, 170, 60],
Shape::Triangle,
);
std::fs::write(
dir.path().join("notes.md"),
"# Notes\n\nSharded vector indexes, quantisation and the write-ahead log.\n",
)
.unwrap();
std::fs::write(
dir.path().join("parse.rs"),
"fn main() { println!(\"tokenising a source file\"); }\n",
)
.unwrap();
dir
}
fn open(store: &Path) -> Semlith {
let mut s = Semlith::open(store, None).unwrap();
s.quiet = true;
s
}
#[test]
#[ignore = "downloads CLIP and an embedding model on first run"]
fn indexing_records_a_row_and_a_vector_for_every_image() {
let corpus = corpus();
let store = tempfile::tempdir().unwrap();
let mut s = open(store.path());
let report = s
.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
assert_eq!(report.images, 3, "three images were indexed");
assert!(report.chunks >= 2, "the text files were chunked as before");
let rows: Vec<(String, u32, u32)> = s
.db()
.prepare("SELECT f.path, i.width, i.height FROM images i JOIN files f ON f.id = i.file_id")
.unwrap()
.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))
.unwrap()
.map(Result::unwrap)
.collect();
assert_eq!(rows.len(), 3);
for (path, width, height) in &rows {
assert_eq!((*width, *height), (224, 224), "{path} has the wrong size");
}
assert_eq!(s.image_count().unwrap(), 3);
assert!(
store.path().join(semlith::image::INDEX_DIR).exists(),
"no image index was written"
);
}
#[test]
#[ignore = "downloads CLIP and an embedding model on first run"]
fn a_sentence_finds_the_image_it_describes() {
let corpus = corpus();
let store = tempfile::tempdir().unwrap();
let mut s = open(store.path());
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
for (query, wanted) in [
("a red circle", "red-circle.png"),
("a blue square", "blue-square.jpg"),
("a green triangle", "green-triangle.webp"),
] {
let hits = s.search(query, 5).unwrap();
let top = hits
.first()
.unwrap_or_else(|| panic!("{query} found nothing"));
assert!(
top.path.ends_with(wanted),
"{query} put {} first, not {wanted}: {:?}",
top.path,
hits.iter().map(|h| h.path.as_str()).collect::<Vec<_>>()
);
let pixels = top.image.expect("an image hit carries its pixel size");
assert_eq!((pixels.width, pixels.height), (224, 224));
assert_eq!(top.start_line, 0, "an image has no line range");
assert!(top.text.is_empty(), "an image has no excerpt");
assert!(top.lists.contains(&"image"), "{:?}", top.lists);
}
}
#[test]
#[ignore = "downloads CLIP and an embedding model on first run"]
fn a_query_about_the_text_still_ranks_the_text_first() {
let corpus = corpus();
let store = tempfile::tempdir().unwrap();
let mut s = open(store.path());
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
let hits = s
.search("sharded vector indexes and quantisation", 5)
.unwrap();
let top = hits.first().expect("the text is there");
assert!(
top.image.is_none(),
"an image outranked the text it has nothing to do with: {}",
top.path
);
}
#[test]
#[ignore = "downloads CLIP and an embedding model on first run"]
fn forgetting_an_image_removes_its_row_and_its_vector() {
let corpus = corpus();
let store = tempfile::tempdir().unwrap();
let mut s = open(store.path());
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
let (chunks, images) = s.forget(&corpus.path().join("red-circle.png")).unwrap();
assert_eq!((chunks, images), (0, 1), "an image has no chunks to remove");
assert_eq!(s.image_count().unwrap(), 2);
let hits = s.search("a red circle", 5).unwrap();
assert!(
!hits.iter().any(|h| h.path.ends_with("red-circle.png")),
"the forgotten image is still searchable"
);
let hits = s.search("a blue square", 5).unwrap();
assert!(hits.iter().any(|h| h.path.ends_with("blue-square.jpg")));
}
#[test]
#[ignore = "downloads CLIP and an embedding model on first run"]
fn re_indexing_an_edited_image_replaces_its_vector() {
let corpus = corpus();
let store = tempfile::tempdir().unwrap();
let mut s = open(store.path());
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
assert_eq!(s.image_count().unwrap(), 3);
shape(
corpus.path(),
"red-circle.png",
[20, 40, 220],
Shape::Square,
);
let report = s
.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
assert_eq!(report.images, 1, "only the edited image was re-embedded");
assert_eq!(s.image_count().unwrap(), 3, "a second row was written");
let size: (u32, u32) = s
.db()
.query_row(
"SELECT i.width, i.height FROM images i JOIN files f ON f.id = i.file_id \
WHERE f.path LIKE '%red-circle.png'",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.unwrap();
assert_eq!(size, (224, 224));
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn a_store_with_no_image_never_reaches_for_clip() {
let corpus = tempfile::tempdir().unwrap();
std::fs::write(
corpus.path().join("only.md"),
"# Only text\n\nNothing here is a picture.\n",
)
.unwrap();
let store = tempfile::tempdir().unwrap();
let mut s = open(store.path());
let report = s
.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
assert_eq!(report.images, 0);
let hits = s.search("anything at all", 5).unwrap();
assert!(hits.iter().all(|h| h.image.is_none()));
assert_eq!(s.image_count().unwrap(), 0);
}
#[test]
fn a_file_that_is_not_an_image_is_skipped() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("broken.png");
std::fs::write(&path, b"this is not a png at all").unwrap();
assert!(semlith::image::is_image(&path));
assert_eq!(
semlith::image::dimensions(b"this is not a png at all"),
None
);
let mut png = Vec::new();
image::DynamicImage::ImageRgb8(image::RgbImage::new(7, 11))
.write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png)
.unwrap();
assert_eq!(semlith::image::dimensions(&png), Some((7, 11)));
}