339 lines
13 KiB
C++
339 lines
13 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright 2025 YAMS Contributors
|
|
//
|
|
// Test: Full pipeline from text file detection → extraction → FTS5 indexing → search
|
|
// Purpose: Isolate issues in the text extraction and indexing pipeline
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/matchers/catch_matchers_string.hpp>
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include <yams/detection/file_type_detector.h>
|
|
#include <yams/extraction/plain_text_extractor.h>
|
|
#include <yams/extraction/text_extractor.h>
|
|
#include <yams/metadata/database.h>
|
|
|
|
using namespace yams;
|
|
using Catch::Matchers::ContainsSubstring;
|
|
|
|
namespace {
|
|
|
|
struct PipelineTestFixture {
|
|
std::filesystem::path tempDir;
|
|
std::filesystem::path dbPath;
|
|
std::filesystem::path txtFile;
|
|
|
|
PipelineTestFixture() {
|
|
tempDir = std::filesystem::temp_directory_path() / "yams_pipeline_test";
|
|
std::filesystem::create_directories(tempDir);
|
|
auto ts = std::chrono::steady_clock::now().time_since_epoch().count();
|
|
dbPath = tempDir / ("pipeline_test_" + std::to_string(ts) + ".db");
|
|
txtFile = tempDir / "test_document.txt";
|
|
}
|
|
|
|
~PipelineTestFixture() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(tempDir, ec);
|
|
}
|
|
|
|
void createTextFile(const std::string& content) {
|
|
std::ofstream out(txtFile);
|
|
out << content;
|
|
out.close();
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Pipeline: MIME type detection for .txt files", "[unit][pipeline][detection]") {
|
|
PipelineTestFixture fix;
|
|
|
|
SECTION("Extension-based detection returns text/plain for .txt") {
|
|
auto mime = detection::FileTypeDetector::getMimeTypeFromExtension(".txt");
|
|
REQUIRE(mime == "text/plain");
|
|
spdlog::info("✓ .txt extension maps to text/plain");
|
|
}
|
|
|
|
SECTION("isTextMimeType returns true for text/plain") {
|
|
auto& detector = detection::FileTypeDetector::instance();
|
|
REQUIRE(detector.isTextMimeType("text/plain"));
|
|
spdlog::info("✓ text/plain is recognized as text MIME type");
|
|
}
|
|
|
|
SECTION("File content detection for plain text without BOM") {
|
|
fix.createTextFile("This is plain text content without any BOM marker.");
|
|
|
|
auto& detector = detection::FileTypeDetector::instance();
|
|
auto result = detector.detectFromFile(fix.txtFile);
|
|
|
|
REQUIRE(result.has_value());
|
|
auto& sig = result.value();
|
|
|
|
// Plain text without BOM should be detected as text/plain
|
|
INFO("Detected MIME: " << sig.mimeType);
|
|
INFO("Is binary: " << sig.isBinary);
|
|
INFO("File type: " << sig.fileType);
|
|
|
|
REQUIRE_FALSE(sig.isBinary);
|
|
// May be text/plain or fall back based on detection
|
|
REQUIRE(detector.isTextMimeType(sig.mimeType));
|
|
spdlog::info("✓ Plain text file detected as text type: {}", sig.mimeType);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Pipeline: PlainTextExtractor extracts .txt content", "[unit][pipeline][extraction]") {
|
|
PipelineTestFixture fix;
|
|
|
|
const std::string testContent = "Scientific claims about gene expression.\n"
|
|
"BRCA1 mutations are associated with breast cancer.\n"
|
|
"The p53 tumor suppressor gene regulates cell cycle.";
|
|
|
|
fix.createTextFile(testContent);
|
|
|
|
SECTION("Extract from file path") {
|
|
extraction::PlainTextExtractor extractor;
|
|
|
|
REQUIRE(extractor.canExtract(fix.txtFile));
|
|
|
|
extraction::ExtractionConfig cfg;
|
|
cfg.preserveFormatting = true;
|
|
|
|
auto result = extractor.extract(fix.txtFile, cfg);
|
|
REQUIRE(result.has_value());
|
|
|
|
auto& extracted = result.value();
|
|
REQUIRE(extracted.isSuccess());
|
|
REQUIRE_FALSE(extracted.text.empty());
|
|
|
|
INFO("Extracted text length: " << extracted.text.size());
|
|
INFO("First 100 chars: " << extracted.text.substr(0, 100));
|
|
|
|
REQUIRE_THAT(extracted.text, ContainsSubstring("BRCA1"));
|
|
REQUIRE_THAT(extracted.text, ContainsSubstring("tumor suppressor"));
|
|
spdlog::info("✓ PlainTextExtractor successfully extracted {} bytes", extracted.text.size());
|
|
}
|
|
|
|
SECTION("Extract from buffer") {
|
|
extraction::PlainTextExtractor extractor;
|
|
|
|
// Read file into buffer
|
|
std::ifstream in(fix.txtFile, std::ios::binary);
|
|
std::vector<char> charBuf((std::istreambuf_iterator<char>(in)),
|
|
std::istreambuf_iterator<char>());
|
|
std::vector<std::byte> buffer;
|
|
buffer.reserve(charBuf.size());
|
|
for (char c : charBuf) {
|
|
buffer.push_back(static_cast<std::byte>(c));
|
|
}
|
|
|
|
extraction::ExtractionConfig cfg;
|
|
auto result = extractor.extractFromBuffer(
|
|
std::span<const std::byte>(buffer.data(), buffer.size()), cfg);
|
|
|
|
REQUIRE(result.has_value());
|
|
auto& extracted = result.value();
|
|
REQUIRE(extracted.isSuccess());
|
|
REQUIRE_THAT(extracted.text, ContainsSubstring("BRCA1"));
|
|
spdlog::info("✓ PlainTextExtractor buffer extraction works");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Pipeline: TextExtractorFactory returns extractor for .txt",
|
|
"[unit][pipeline][factory]") {
|
|
auto& factory = extraction::TextExtractorFactory::instance();
|
|
|
|
SECTION("Factory supports .txt extension") {
|
|
REQUIRE(factory.isSupported(".txt"));
|
|
spdlog::info("✓ TextExtractorFactory supports .txt");
|
|
}
|
|
|
|
SECTION("Factory creates extractor for .txt") {
|
|
auto extractor = factory.create(".txt");
|
|
REQUIRE(extractor != nullptr);
|
|
INFO("Extractor name: " << extractor->name());
|
|
spdlog::info("✓ Factory created extractor: {}", extractor->name());
|
|
}
|
|
|
|
SECTION("Factory handles case-insensitive extensions") {
|
|
REQUIRE(factory.isSupported(".TXT"));
|
|
REQUIRE(factory.isSupported(".Txt"));
|
|
auto extractor = factory.create(".TXT");
|
|
REQUIRE(extractor != nullptr);
|
|
spdlog::info("✓ Case-insensitive extension lookup works");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Pipeline: FTS5 indexes and searches extracted text", "[unit][pipeline][fts5]") {
|
|
PipelineTestFixture fix;
|
|
|
|
const std::string scientificContent =
|
|
"BRCA1 mutations significantly increase breast cancer risk.\n"
|
|
"The study found that p53 pathway dysregulation leads to tumor progression.\n"
|
|
"Gene expression analysis revealed differential markers.";
|
|
|
|
fix.createTextFile(scientificContent);
|
|
|
|
// Step 1: Extract text
|
|
extraction::PlainTextExtractor extractor;
|
|
extraction::ExtractionConfig cfg;
|
|
cfg.preserveFormatting = true;
|
|
auto extractResult = extractor.extract(fix.txtFile, cfg);
|
|
REQUIRE(extractResult.has_value());
|
|
REQUIRE(extractResult.value().isSuccess());
|
|
std::string extractedText = extractResult.value().text;
|
|
|
|
INFO("Extracted text: " << extractedText);
|
|
|
|
// Step 2: Create FTS5 table and index
|
|
metadata::Database db;
|
|
auto openResult = db.open(fix.dbPath.string(), metadata::ConnectionMode::Create);
|
|
REQUIRE(openResult.has_value());
|
|
|
|
auto hasFts5 = db.hasFTS5();
|
|
REQUIRE(hasFts5.has_value());
|
|
if (!hasFts5.value()) {
|
|
SKIP("FTS5 not available");
|
|
}
|
|
|
|
// Create FTS5 table matching YAMS schema
|
|
auto createResult = db.execute("CREATE VIRTUAL TABLE documents_fts USING fts5("
|
|
"content, title, "
|
|
"tokenize='porter unicode61 tokenchars ''_-'''"
|
|
")");
|
|
REQUIRE(createResult.has_value());
|
|
|
|
// Insert extracted content
|
|
auto insertStmt =
|
|
db.prepare("INSERT INTO documents_fts(rowid, content, title) VALUES (?, ?, ?)");
|
|
REQUIRE(insertStmt.has_value());
|
|
auto& stmt = insertStmt.value();
|
|
stmt.bind(1, 12345); // Document ID
|
|
stmt.bind(2, extractedText);
|
|
stmt.bind(3, "test_document.txt");
|
|
auto insertResult = stmt.execute();
|
|
REQUIRE(insertResult.has_value());
|
|
|
|
spdlog::info("✓ Indexed {} bytes into FTS5", extractedText.size());
|
|
|
|
SECTION("Search for exact term: BRCA1") {
|
|
auto searchStmt =
|
|
db.prepare("SELECT rowid, snippet(documents_fts, 0, '<b>', '</b>', '...', 16) "
|
|
"FROM documents_fts WHERE documents_fts MATCH 'BRCA1'");
|
|
REQUIRE(searchStmt.has_value());
|
|
|
|
auto stepResult = searchStmt.value().step();
|
|
REQUIRE(stepResult.has_value());
|
|
REQUIRE(stepResult.value()); // Has results
|
|
spdlog::info("✓ FTS5 search for 'BRCA1' returned results");
|
|
}
|
|
|
|
SECTION("Search for stemmed term: mutations") {
|
|
auto searchStmt =
|
|
db.prepare("SELECT rowid FROM documents_fts WHERE documents_fts MATCH 'mutation'");
|
|
REQUIRE(searchStmt.has_value());
|
|
|
|
auto stepResult = searchStmt.value().step();
|
|
REQUIRE(stepResult.has_value());
|
|
INFO("Step result: " << stepResult.value());
|
|
// Porter stemmer should match 'mutations' with 'mutation'
|
|
REQUIRE(stepResult.value());
|
|
spdlog::info("✓ FTS5 porter stemmer matches 'mutation' -> 'mutations'");
|
|
}
|
|
|
|
SECTION("Search for phrase: breast cancer") {
|
|
auto searchStmt = db.prepare(
|
|
"SELECT rowid FROM documents_fts WHERE documents_fts MATCH '\"breast cancer\"'");
|
|
REQUIRE(searchStmt.has_value());
|
|
|
|
auto stepResult = searchStmt.value().step();
|
|
REQUIRE(stepResult.has_value());
|
|
REQUIRE(stepResult.value());
|
|
spdlog::info("✓ FTS5 phrase search works");
|
|
}
|
|
|
|
SECTION("Search for multiple terms (AND): BRCA1 cancer") {
|
|
auto searchStmt =
|
|
db.prepare("SELECT rowid FROM documents_fts WHERE documents_fts MATCH 'BRCA1 cancer'");
|
|
REQUIRE(searchStmt.has_value());
|
|
|
|
auto stepResult = searchStmt.value().step();
|
|
REQUIRE(stepResult.has_value());
|
|
REQUIRE(stepResult.value());
|
|
spdlog::info("✓ FTS5 multi-term AND search works");
|
|
}
|
|
|
|
SECTION("Search returns no results for non-existent term") {
|
|
auto searchStmt = db.prepare(
|
|
"SELECT rowid FROM documents_fts WHERE documents_fts MATCH 'xyznonexistent'");
|
|
REQUIRE(searchStmt.has_value());
|
|
|
|
auto stepResult = searchStmt.value().step();
|
|
REQUIRE(stepResult.has_value());
|
|
REQUIRE_FALSE(stepResult.value()); // No results expected
|
|
spdlog::info("✓ FTS5 correctly returns no results for non-existent term");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Pipeline: Scientific query patterns", "[unit][pipeline][scientific]") {
|
|
PipelineTestFixture fix;
|
|
|
|
// Content similar to scifact BEIR dataset
|
|
const std::string content =
|
|
"Polymeal nutrition has been shown to reduce cardiovascular mortality "
|
|
"in multiple clinical trials. The combination of wine, fish, dark chocolate, "
|
|
"fruits, vegetables, garlic, and almonds provides synergistic benefits.";
|
|
|
|
fix.createTextFile(content);
|
|
|
|
// Extract
|
|
extraction::PlainTextExtractor extractor;
|
|
auto extractResult = extractor.extract(fix.txtFile, {});
|
|
REQUIRE(extractResult.has_value());
|
|
std::string text = extractResult.value().text;
|
|
|
|
// Index
|
|
metadata::Database db;
|
|
REQUIRE(db.open(fix.dbPath.string(), metadata::ConnectionMode::Create).has_value());
|
|
auto hasFts = db.hasFTS5();
|
|
if (!hasFts.has_value() || !hasFts.value())
|
|
SKIP("FTS5 not available");
|
|
|
|
REQUIRE(db.execute("CREATE VIRTUAL TABLE docs USING fts5(content, tokenize='porter unicode61')")
|
|
.has_value());
|
|
|
|
auto ins = db.prepare("INSERT INTO docs(rowid, content) VALUES (1, ?)");
|
|
REQUIRE(ins.has_value());
|
|
ins.value().bind(1, text);
|
|
REQUIRE(ins.value().execute().has_value());
|
|
|
|
SECTION("Query: Polymeal nutrition reduces cardiovascular mortality") {
|
|
// This is a typical scifact query - test different formulations
|
|
|
|
// Full query (may fail due to too many terms)
|
|
auto q1 = db.prepare("SELECT rowid FROM docs WHERE docs MATCH 'polymeal nutrition "
|
|
"cardiovascular mortality'");
|
|
REQUIRE(q1.has_value());
|
|
auto r1 = q1.value().step();
|
|
INFO("Full query result: " << (r1.has_value() ? (r1.value() ? "FOUND" : "NOT FOUND")
|
|
: "ERROR"));
|
|
|
|
// Single key term
|
|
auto q2 = db.prepare("SELECT rowid FROM docs WHERE docs MATCH 'polymeal'");
|
|
REQUIRE(q2.has_value());
|
|
auto r2 = q2.value().step();
|
|
REQUIRE(r2.has_value());
|
|
REQUIRE(r2.value()); // Must find 'polymeal'
|
|
spdlog::info("✓ Single term 'polymeal' found");
|
|
|
|
// Two terms
|
|
auto q3 = db.prepare("SELECT rowid FROM docs WHERE docs MATCH 'cardiovascular mortality'");
|
|
REQUIRE(q3.has_value());
|
|
auto r3 = q3.value().step();
|
|
REQUIRE(r3.has_value());
|
|
REQUIRE(r3.value());
|
|
spdlog::info("✓ Two terms 'cardiovascular mortality' found");
|
|
}
|
|
}
|