SemanticRetrievalService.kt
src/jvmMain/kotlin/afield/ai/retrieval/SemanticRetrievalService.kt · 2540 lines
package and imports — 67 lines, from line 1
package afield.ai.retrieval
import afield.ai.knowledge.main
import afield.ai.conversation.*
import afield.ai.models.*
import afield.graphics.render.*
import afield.runtime.completion.KotlinDefinitionRecord
import afield.runtime.completion.KotlinSourceIndex
import afield.runtime.completion.KotlinSourceIndexDb
import ai.djl.huggingface.tokenizers.Encoding
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer
import ai.onnxruntime.OnnxTensor
import ai.onnxruntime.OrtEnvironment
import ai.onnxruntime.OrtSession
import ai.onnxruntime.providers.CoreMLFlags
import afield.document.graph.Box
import afield.document.persistence.SavingContext
import afield.app.server.broadcastWelcomeEvent
import afield.app.server.currentFieldPreferences
import afield.app.server.home
import afield.app.diagnostics.logging.lprintln
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.apache.lucene.analysis.Analyzer
import org.apache.lucene.analysis.standard.StandardAnalyzer
import org.apache.lucene.document.Document
import org.apache.lucene.document.Field
import org.apache.lucene.document.KnnFloatVectorField
import org.apache.lucene.document.StoredField
import org.apache.lucene.document.StringField
import org.apache.lucene.document.TextField
import org.apache.lucene.index.DirectoryReader
import org.apache.lucene.index.IndexWriter
import org.apache.lucene.index.IndexWriterConfig
import org.apache.lucene.index.Term
import org.apache.lucene.index.VectorSimilarityFunction
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser
import org.apache.lucene.search.BooleanClause
import org.apache.lucene.search.BooleanQuery
import org.apache.lucene.search.IndexSearcher
import org.apache.lucene.search.KnnFloatVectorQuery
import org.apache.lucene.search.Query
import org.apache.lucene.search.ScoreDoc
import org.apache.lucene.search.TermInSetQuery
import org.apache.lucene.search.TermQuery
import org.apache.lucene.util.BytesRef
import org.apache.lucene.store.FSDirectory
import java.nio.charset.StandardCharsets
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.security.MessageDigest
import java.time.Instant
import java.util.EnumSet
import java.util.Locale
import kotlin.concurrent.thread
import kotlin.io.path.absolutePathString
import kotlin.io.path.createDirectories
import kotlin.io.path.exists
import kotlin.math.sqrt
Hybrid semantic search over Field's own source, the open workspace and the documentation, backed by a local Lucene index and an ONNX embedding model.
Each indexed item is a SemanticCapsule carrying an intent text, an implementation text and a
lexical text. SemanticRetrievalService.query runs BM25 and symbol matches alongside vector
nearest-neighbour searches on the intent and implementation embeddings, fuses the ranks, and
filters by corpus (field-internals, workspace, documentation), path, kind, document, box
and property.
Models and cache
OnnxSemanticEmbedder implements SemanticEmbedder with a Hugging Face tokenizer and ONNX
Runtime. The model presets download from Hugging Face through
SemanticRetrievalService.downloadPreset; models, settings and the index live under
~/.afield/semantic-search unless afield.semanticSearch.cacheRoot (or
AFIELD_SEMANTIC_SEARCH_CACHE_ROOT) says otherwise.
Rebuilding
SemanticRetrievalService.rebuild and SemanticRetrievalService.startRebuild rebuild the index; SemanticRetrievalService.startIdleRebuildWatcher rebuilds when workspace documents change.
source — 41 lines, from line 93
private val semanticSearchProviders = setOf("cpu", "coreml", "coreml-mlprogram", "xnnpack")
private fun semanticSearchProviderSetting(): String =
listOf(
System.getProperty("afield.semanticSearch.provider"),
System.getenv("AFIELD_SEMANTIC_SEARCH_PROVIDER")
).firstOrNull { !it.isNullOrBlank() }
?.trim()
?.lowercase(Locale.ROOT)
?.takeIf { it in semanticSearchProviders }
?: defaultSemanticSearchProvider()
private fun defaultSemanticSearchProvider(): String {
val osName = System.getProperty("os.name").orEmpty().lowercase(Locale.ROOT)
val osArch = System.getProperty("os.arch").orEmpty().lowercase(Locale.ROOT)
return if (osName.contains("mac") && (osArch == "aarch64" || osArch == "arm64")) {
"xnnpack"
} else {
"cpu"
}
}
private fun semanticSearchFixedShapesSetting(provider: String): Boolean =
listOf(
System.getProperty("afield.semanticSearch.fixedShapes"),
System.getenv("AFIELD_SEMANTIC_SEARCH_FIXED_SHAPES")
).firstOrNull { !it.isNullOrBlank() }
?.trim()
?.lowercase(Locale.ROOT)
?.let { it in setOf("1", "true", "yes", "on") }
?: provider.startsWith("coreml")
private fun semanticSearchBatchSizeSetting(provider: String = semanticSearchProviderSetting()): Int =
listOf(
System.getProperty("afield.semanticSearch.batchSize"),
System.getenv("AFIELD_SEMANTIC_SEARCH_BATCH_SIZE")
).firstOrNull { !it.isNullOrBlank() }
?.trim()
?.toIntOrNull()
?.coerceIn(1, 64)
?: if (provider == "xnnpack") 64 else 8
data class SemanticRetrievalStatus
@Serializable
data class SemanticRetrievalStatus(
val enabled: Boolean,
val indexPath: String,
val indexExists: Boolean,
val indexedDocuments: Int = 0,
val indexedCapsules: Int = 0,
val capsuleBackend: String = "source-index-capsules",
val embedderBackend: String = "disabled",
val embeddingDimension: Int = 0,
val lastBuildEpochMs: Long = 0,
val workspaceRoot: String = "",
val workspaceDocuments: Int = 0,
val workspaceSidecarFiles: Int = 0,
val corpusFingerprint: String = "",
val currentCorpusFingerprint: String = "",
val indexStale: Boolean = false,
// Workspace documents added, removed or saved since the index was built (-1: the index predates this count).
// Docs pages and source are not counted; [indexStale] covers them.
val staleDocuments: Int = -1,
val message: String = ""
)
data class SemanticRetrievalConfig
@Serializable
data class SemanticRetrievalConfig(
val enabled: Boolean = true,
val presetId: String = "granite-97m-r2-quantized",
val modelPath: String = "",
val tokenizerPath: String = "",
val dimension: Int = 384,
val maxTokens: Int = 512,
val queryPrefix: String = "",
val documentPrefix: String = ""
)
data class SemanticRetrievalModelPreset
@Serializable
data class SemanticRetrievalModelPreset(
val id: String,
val label: String,
val modelUrl: String,
val tokenizerUrl: String,
val modelFileName: String,
val tokenizerFileName: String,
val dimension: Int,
val maxTokens: Int,
val queryPrefix: String = "",
val documentPrefix: String = "",
val note: String = ""
)
data class SemanticRetrievalFileState
@Serializable
data class SemanticRetrievalFileState(
val path: String = "",
val exists: Boolean = false,
val sizeBytes: Long = 0,
val lastModifiedEpochMs: Long = 0
)
data class SemanticRetrievalSettingsStatus
@Serializable
data class SemanticRetrievalSettingsStatus(
val config: SemanticRetrievalConfig,
val status: SemanticRetrievalStatus,
val model: SemanticRetrievalModelInfo,
val presets: List<SemanticRetrievalModelPreset>,
val modelFile: SemanticRetrievalFileState,
val tokenizerFile: SemanticRetrievalFileState,
val indexSizeBytes: Long,
val modelCacheSizeBytes: Long,
val configPath: String,
val cacheRoot: String,
val downloadRunning: Boolean = false,
val rebuild: SemanticRetrievalBuildStatus = SemanticRetrievalBuildStatus(),
val message: String = ""
)
data class SemanticRetrievalBuildStatus
@Serializable
data class SemanticRetrievalBuildStatus(
val running: Boolean = false,
val status: String = "idle",
val startedAtEpochMs: Long = 0,
val finishedAtEpochMs: Long = 0,
val message: String = "",
val step: String = "",
val indexedCapsules: Int = 0
)
data class SemanticRetrievalBuildResult
@Serializable
data class SemanticRetrievalBuildResult(
val success: Boolean,
val message: String,
val status: SemanticRetrievalStatus,
val rebuild: SemanticRetrievalBuildStatus = SemanticRetrievalBuildStatus()
)
data class SemanticRetrievalQueryRequest
@Serializable
data class SemanticRetrievalQueryRequest(
val query: String,
val limit: Int = 10,
val corpus: String = "",
val corpora: List<String> = emptyList(),
val pathPrefix: String = "",
val kinds: List<String> = emptyList(),
val documentName: String = "",
val boxId: String = "",
val property: String = "",
val rebuildIfEmpty: Boolean = true,
val includeCapsule: Boolean = false,
val includeSource: Boolean = false
)
data class SemanticRetrievalQueryResult
@Serializable
data class SemanticRetrievalQueryResult(
val success: Boolean,
val message: String,
val status: SemanticRetrievalStatus,
val results: List<SemanticRetrievalHit> = emptyList()
)
data class SemanticRetrievalHit
@Serializable
data class SemanticRetrievalHit(
val id: String,
val scale: String,
val corpus: String,
val kind: String,
val symbol: String,
val title: String,
val path: String,
val line: Int,
val endLine: Int = 0,
val parentId: String = "",
val documentName: String = "",
val boxName: String = "",
val boxId: String = "",
val property: String = "",
val language: String = "",
val score: Double,
val vectorRank: Int = 0,
val bm25Rank: Int = 0,
val symbolRank: Int = 0,
val vectorScore: Float = 0f,
val bm25Score: Float = 0f,
val symbolScore: Float = 0f,
val capsule: String = "",
val sourcePreview: String = ""
)
data class SemanticRetrievalModelInfo
@Serializable
data class SemanticRetrievalModelInfo(
val backend: String,
val dimension: Int,
val modelPath: String = "",
val tokenizerPath: String = "",
val queryPrefix: String = "",
val documentPrefix: String = "",
val message: String = ""
)
data class SemanticRetrievalDownloadRequest
@Serializable
data class SemanticRetrievalDownloadRequest(
val presetId: String = "granite-97m-r2-quantized"
)
data class SemanticRetrievalConfigSaveRequest
@Serializable
data class SemanticRetrievalConfigSaveRequest(
val enabled: Boolean? = null,
val presetId: String? = null,
val modelPath: String? = null,
val tokenizerPath: String? = null,
val dimension: Int? = null,
val maxTokens: Int? = null,
val queryPrefix: String? = null,
val documentPrefix: String? = null
)
data class SemanticRetrievalDeleteCacheRequest
@Serializable
data class SemanticRetrievalDeleteCacheRequest(
val target: String = "index"
)
data class SemanticCapsule
data class SemanticCapsule(
val id: String,
val scale: String,
val corpus: String,
val kind: String,
val symbol: String,
val title: String,
val path: String,
val line: Int,
val endLine: Int = 0,
val parentId: String = "",
val documentName: String = "",
val boxName: String = "",
val boxId: String = "",
val property: String = "",
val language: String = "",
val signature: String = "",
val documentation: String = "",
val metadata: String = "",
val source: String = ""
) {
fun intentText(): String
fun intentText(): String = buildString {
appendLine("kind: $kind")
appendLine("symbol: $symbol")
appendLine("title: $title")
appendLine("corpus: $corpus")
appendLine("file: $path")
if (documentName.isNotBlank()) appendLine("document: $documentName")
if (boxName.isNotBlank()) appendLine("box: $boxName")
if (boxId.isNotBlank()) appendLine("box_uid: $boxId")
if (property.isNotBlank()) appendLine("property: $property")
if (language.isNotBlank()) appendLine("language: $language")
if (signature.isNotBlank()) {
appendLine()
appendLine("signature:")
appendLine(signature)
}
if (documentation.isNotBlank()) {
appendLine()
appendLine("documentation:")
appendLine(documentation)
}
if (metadata.isNotBlank()) {
appendLine()
appendLine("metadata:")
appendLine(metadata)
}
}.trim()
fun implementationText(): String
fun implementationText(): String = buildString {
if (signature.isNotBlank()) {
appendLine("signature:")
appendLine(signature)
appendLine()
}
if (source.isNotBlank()) {
appendLine("source:")
appendLine(source)
} else {
append(intentText())
}
}.trim()
fun lexicalText(): String
fun lexicalText(): String = listOf(
symbol,
title,
kind,
path,
documentName,
boxName,
boxId,
property,
language,
signature,
documentation,
metadata,
source
)
.filter { it.isNotBlank() }
.joinToString("\n")
}
interface SemanticEmbedder : AutoCloseable
val dimension: Int
val backend: String
fun encodeQueries(texts: List<String>): List<FloatArray>
fun encodeDocuments(texts: List<String>): List<FloatArray>
object SemanticRetrievalService
object SemanticRetrievalService {
private const val INTENT_VECTOR = "intentVector"
internal const val VECTOR_FORMAT = "2"
Bumped when indexed vectors change meaning. 2: the first index with real vectors (2026-10-22); every index before it holds zero vectors (see OnnxSemanticEmbedder's output read), so it is stale and its vectors are not searched.
The weight of the stemmed copy against the exact text; see indexAnalyzer. Measured 2026-10-22 over
0…3: every set improves to 1.0, anchor and why then plateau and tutorials keep rising slowly; 1.5–3.0 are
within 0.004 MRR of each other weighted by cases, so this is the middle of a plateau, not a tuned peak.
private const val DEFAULT_STEM_BOOST = 2.0f
private const val IMPLEMENTATION_VECTOR = "implementationVector"
private const val META_FILE = "field-semantic-search.meta"
One name:mtime:size line per workspace document, as seen when the index was built.
private const val DOCUMENTS_FILE = "field-semantic-search.documents"
private const val SETTINGS_STATUS_TTL_MS = 15_000L
val availableCorpora: List<String>
val availableCorpora: List<String> = listOf("field-internals", "workspace", "documentation", "tutorials")
private val cacheRoot: Path = Paths.get(
System.getProperty("afield.semanticSearch.cacheRoot")
?: System.getenv("AFIELD_SEMANTIC_SEARCH_CACHE_ROOT")
?: Paths.get(System.getProperty("user.home"), ".afield", "semantic-search").toString()
).toAbsolutePath().normalize()
private val modelCacheRoot: Path = cacheRoot.resolve("models").toAbsolutePath().normalize()
private val configPath: Path = cacheRoot.resolve("config.json").toAbsolutePath().normalize()
private val explicitIndexPath: Path? = (
System.getProperty("afield.semanticSearch.indexPath")
?: System.getenv("AFIELD_SEMANTIC_SEARCH_INDEX_PATH")
)?.trim()?.takeIf { it.isNotBlank() }?.let { Paths.get(it).toAbsolutePath().normalize() }
Measurement switches for RetrievalEval, read on every call so one JVM can compare settings:
afield.semanticSearch.fusion = hybrid (default) | lexical | dense; afield.semanticSearch.chunker =
chunks (default, MarkdownChunker) | sections (the cut before 2026-10-22); afield.semanticSearch.corpora
= a comma list a rebuild is limited to (a documentation-only index embeds in minutes, the whole one in hours).
private fun evalSetting(name: String): String? = System.getProperty("afield.semanticSearch.$name")?.trim()?.takeIf { it.isNotBlank() }
private val analyzer: Analyzer = StandardAnalyzer()
The lexical text is indexed twice: as written (every field, analyzer) and in allStem, split and stemmed
(codeAwareAnalyzer: fline_measure → fline_measure, fline, measure; computeModuleHere → compute, module,
here; "FLines" → fline). Measured alone, the stemmed form raised top-5 recall everywhere and lost first
place (anchor hit@1 56.1% → 50.9%); together, with allStem down-weighted by stemBoost, exact matches
keep first place and stemmed ones fill in.
source — 48 lines, from line 453
private val indexAnalyzer: Analyzer by lazy {
org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper(analyzer, mapOf("allStem" to codeAwareAnalyzer()))
}
private val stemAnalyzer: Analyzer by lazy { codeAwareAnalyzer() }
private fun stemBoost(): Float = evalSetting("stemBoost")?.toFloatOrNull() ?: DEFAULT_STEM_BOOST
private fun codeAwareAnalyzer(): Analyzer = object : Analyzer() {
override fun createComponents(fieldName: String): TokenStreamComponents {
val source = org.apache.lucene.analysis.standard.StandardTokenizer()
var stream: org.apache.lucene.analysis.TokenStream = org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilter(
source,
org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilter.GENERATE_WORD_PARTS or
org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilter.SPLIT_ON_CASE_CHANGE or
org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilter.SPLIT_ON_NUMERICS or
org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilter.PRESERVE_ORIGINAL,
null)
stream = org.apache.lucene.analysis.core.FlattenGraphFilter(stream)
stream = org.apache.lucene.analysis.LowerCaseFilter(stream)
stream = org.apache.lucene.analysis.en.EnglishPossessiveFilter(stream)
stream = org.apache.lucene.analysis.en.PorterStemFilter(stream)
return TokenStreamComponents(source, stream)
}
}
private val json = Json {
encodeDefaults = true
ignoreUnknownKeys = true
prettyPrint = true
}
private val downloadClient: HttpClient = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build()
private val lock = Any()
private val rebuildLock = Any()
@Volatile
private var lastStatus: SemanticRetrievalStatus? = null
@Volatile
private var embedder: SemanticEmbedder? = null
@Volatile
private var embedderMessage: String = ""
@Volatile
private var downloadRunning: Boolean = false
@Volatile
private var rebuildStatus: SemanticRetrievalBuildStatus = SemanticRetrievalBuildStatus()
val presets: List<SemanticRetrievalModelPreset>
val presets: List<SemanticRetrievalModelPreset> = listOf(
SemanticRetrievalModelPreset(
id = "granite-97m-r2-quantized",
label = "Granite 97M R2 int8",
modelUrl = "https://huggingface.co/onnx-community/granite-embedding-97m-multilingual-r2-ONNX/resolve/main/onnx/model_quantized.onnx",
tokenizerUrl = "https://huggingface.co/onnx-community/granite-embedding-97m-multilingual-r2-ONNX/resolve/main/tokenizer.json",
modelFileName = "granite-embedding-97m-multilingual-r2/model_quantized.onnx",
tokenizerFileName = "granite-embedding-97m-multilingual-r2/tokenizer.json",
dimension = 384,
maxTokens = 512,
note = "Small JVM-friendly baseline; quantized ONNX."
),
SemanticRetrievalModelPreset(
id = "granite-97m-r2-fp32",
label = "Granite 97M R2 fp32",
modelUrl = "https://huggingface.co/onnx-community/granite-embedding-97m-multilingual-r2-ONNX/resolve/main/onnx/model.onnx",
tokenizerUrl = "https://huggingface.co/onnx-community/granite-embedding-97m-multilingual-r2-ONNX/resolve/main/tokenizer.json",
modelFileName = "granite-embedding-97m-multilingual-r2/model.onnx",
tokenizerFileName = "granite-embedding-97m-multilingual-r2/tokenizer.json",
dimension = 384,
maxTokens = 512,
note = "Larger baseline with unquantized ONNX weights."
)
)
fun modelInfo(): SemanticRetrievalModelInfo
fun modelInfo(): SemanticRetrievalModelInfo {
val settings = currentConfig()
val configured = OnnxSemanticEmbedder.Config.fromSettings(settings)
val modelPath = configured.modelPath
val tokenizerPath = configured.tokenizerPath
if (settings.enabled && modelPath?.exists() == true && tokenizerPath?.exists() == true) {
val fixedSuffix = if (configured.fixedTokenLength > 0) {
":fixed-${configured.fixedBatchSize}x${configured.fixedTokenLength}"
} else {
""
}
return SemanticRetrievalModelInfo(
backend = "onnx-runtime-java:${configured.provider}$fixedSuffix",
dimension = configured.dimension,
modelPath = modelPath.absolutePathString(),
tokenizerPath = tokenizerPath.absolutePathString(),
queryPrefix = configured.queryPrefix,
documentPrefix = configured.documentPrefix,
message = "ONNX embedder files are configured; runtime session loads on rebuild/query."
)
}
return SemanticRetrievalModelInfo(
backend = if (settings.enabled) "disabled" else "off",
dimension = configured.dimension,
modelPath = configured.modelPath?.absolutePathString().orEmpty(),
tokenizerPath = configured.tokenizerPath?.absolutePathString().orEmpty(),
queryPrefix = configured.queryPrefix,
documentPrefix = configured.documentPrefix,
message = OnnxSemanticEmbedder.Config.disabledReason(configured)
)
}
fun status(): SemanticRetrievalStatus
fun status(): SemanticRetrievalStatus {
val cached = lastStatus
if (cached != null) return cached.copy(indexExists = indexExists())
return computeStatus("Semantic retrieval index has not been built in this process.")
}
settingsStatus() is polled by the welcome screen (and re-fetched on every workspace event). Beyond the fingerprint it opens the Lucene index twice and does two recursive directory-size walks, so serve a short-lived cached copy to collapse bursts of polls into one computation.
@Volatile
private var settingsStatusCache: Pair<Long, SemanticRetrievalSettingsStatus>? = null
Single-flight guard for background settings-status recomputes from settingsStatusSnapshot().
private val settingsRefreshLock = Any()
private var settingsRefreshing = false
fun settingsStatus(): SemanticRetrievalSettingsStatus
fun settingsStatus(): SemanticRetrievalSettingsStatus {
settingsStatusCache?.let {
if (System.currentTimeMillis() - it.first < SETTINGS_STATUS_TTL_MS) return it.second
}
val computed = computeSettingsStatus()
settingsStatusCache = System.currentTimeMillis() to computed
return computed
}
fun settingsStatusSnapshot(): SemanticRetrievalSettingsStatus
Non-blocking counterpart to settingsStatus for the welcome screen. Returns the last computed status immediately (or a cheap placeholder before the first compute finishes) and refreshes in the background when stale. Keeps the one-time corpus-fingerprint rescan and the Lucene/directory-size walks off the caller's thread so they can't stall the shared workspace-search dispatcher — and, through it, a document the user opens right after.
body — 72 lines, from line 591
fun settingsStatusSnapshot(): SemanticRetrievalSettingsStatus {
val cached = settingsStatusCache
if (cached == null || System.currentTimeMillis() - cached.first >= SETTINGS_STATUS_TTL_MS) {
triggerSettingsRefresh()
}
return cached?.second ?: settingsStatusPlaceholder()
}
private fun triggerSettingsRefresh() {
synchronized(settingsRefreshLock) {
if (settingsRefreshing) return
settingsRefreshing = true
}
thread(name = "semantic-settings-status", isDaemon = true) {
try {
val computed = computeSettingsStatus()
settingsStatusCache = System.currentTimeMillis() to computed
} catch (_: Throwable) {
// Keep the prior snapshot; the next poll retries.
} finally {
synchronized(settingsRefreshLock) { settingsRefreshing = false }
}
}
}
private fun settingsStatusPlaceholder(): SemanticRetrievalSettingsStatus {
val config = currentConfig()
val model = OnnxSemanticEmbedder.Config.fromSettings(config)
val status = lastStatus ?: SemanticRetrievalStatus(
enabled = config.enabled,
indexPath = indexPath().absolutePathString(),
indexExists = false,
message = "Gathering semantic search status…"
)
return SemanticRetrievalSettingsStatus(
config = config,
status = status,
model = modelInfo(),
presets = presets,
modelFile = fileState(model.modelPath?.absolutePathString().orEmpty()),
tokenizerFile = fileState(model.tokenizerPath?.absolutePathString().orEmpty()),
indexSizeBytes = 0L,
modelCacheSizeBytes = 0L,
configPath = configPath.absolutePathString(),
cacheRoot = cacheRoot.absolutePathString(),
downloadRunning = downloadRunning,
rebuild = rebuildStatus,
message = "Gathering semantic search status…"
)
}
private fun computeSettingsStatus(): SemanticRetrievalSettingsStatus {
val config = currentConfig()
val model = OnnxSemanticEmbedder.Config.fromSettings(config)
val effectiveModel = model.modelPath?.absolutePathString().orEmpty()
val effectiveTokenizer = model.tokenizerPath?.absolutePathString().orEmpty()
return SemanticRetrievalSettingsStatus(
config = config,
status = computeStatus(""),
model = modelInfo(),
presets = presets,
modelFile = fileState(effectiveModel),
tokenizerFile = fileState(effectiveTokenizer),
indexSizeBytes = directorySize(indexPath()),
modelCacheSizeBytes = directorySize(modelCacheRoot),
configPath = configPath.absolutePathString(),
cacheRoot = cacheRoot.absolutePathString(),
downloadRunning = downloadRunning,
rebuild = rebuildStatus,
message = ""
)
}
fun saveConfig(request: SemanticRetrievalConfigSaveRequest): SemanticRetrievalSettingsStatus
fun saveConfig(request: SemanticRetrievalConfigSaveRequest): SemanticRetrievalSettingsStatus = synchronized(lock) {
val current = currentConfig()
val next = current.copy(
enabled = request.enabled ?: current.enabled,
presetId = request.presetId?.ifBlank { current.presetId } ?: current.presetId,
modelPath = request.modelPath?.trim() ?: current.modelPath,
tokenizerPath = request.tokenizerPath?.trim() ?: current.tokenizerPath,
dimension = request.dimension?.coerceIn(1, 1024) ?: current.dimension,
maxTokens = request.maxTokens?.coerceIn(16, 8192) ?: current.maxTokens,
queryPrefix = request.queryPrefix ?: current.queryPrefix,
documentPrefix = request.documentPrefix ?: current.documentPrefix
)
persistConfig(next)
resetEmbedder()
settingsStatus().copy(message = "Semantic search settings saved.")
}
fun downloadPreset(request: SemanticRetrievalDownloadRequest): SemanticRetrievalSettingsStatus
fun downloadPreset(request: SemanticRetrievalDownloadRequest): SemanticRetrievalSettingsStatus = synchronized(lock) {
val preset = presets.firstOrNull { it.id == request.presetId }
?: error("Unknown semantic search model preset '${request.presetId}'.")
downloadRunning = true
try {
val modelPath = modelCacheRoot.resolve(preset.modelFileName).toAbsolutePath().normalize()
val tokenizerPath = modelCacheRoot.resolve(preset.tokenizerFileName).toAbsolutePath().normalize()
downloadFile(preset.modelUrl, modelPath)
downloadFile(preset.tokenizerUrl, tokenizerPath)
persistConfig(
currentConfig().copy(
enabled = true,
presetId = preset.id,
modelPath = modelPath.absolutePathString(),
tokenizerPath = tokenizerPath.absolutePathString(),
dimension = preset.dimension,
maxTokens = preset.maxTokens,
queryPrefix = preset.queryPrefix,
documentPrefix = preset.documentPrefix
)
)
resetEmbedder()
downloadRunning = false
settingsStatus().copy(message = "Downloaded ${preset.label}.")
} catch (t: Throwable) {
downloadRunning = false
throw t
}
}
fun deleteCache(request: SemanticRetrievalDeleteCacheRequest): SemanticRetrievalSettingsStatus
body — 174 lines, from line 711
fun deleteCache(request: SemanticRetrievalDeleteCacheRequest): SemanticRetrievalSettingsStatus = synchronized(lock) {
when (request.target.trim().lowercase(Locale.ROOT)) {
"index", "lucene" -> deleteRecursively(indexPath())
"models", "model" -> {
resetEmbedder()
deleteRecursively(modelCacheRoot)
}
"all" -> {
resetEmbedder()
deleteRecursively(indexPath())
deleteRecursively(modelCacheRoot)
}
else -> error("Unknown semantic search cache target '${request.target}'.")
}
invalidateStatusCaches()
settingsStatus().copy(message = "Deleted semantic search ${request.target} cache.")
}
private fun beginRebuild(message: String): SemanticRetrievalBuildStatus? = synchronized(rebuildLock) {
if (rebuildStatus.running) return@synchronized null
val status = SemanticRetrievalBuildStatus(
running = true,
status = "running",
startedAtEpochMs = Instant.now().toEpochMilli(),
message = message,
step = "starting"
)
rebuildStatus = status
broadcastWelcomeEvent("semantic-search-rebuild")
status
}
private fun updateRebuildStatus(
step: String,
message: String,
indexedCapsules: Int = rebuildStatus.indexedCapsules
) {
rebuildStatus = rebuildStatus.copy(
running = true,
status = "running",
message = message,
step = step,
indexedCapsules = indexedCapsules
)
broadcastWelcomeEvent("semantic-search-rebuild")
}
private fun finishRebuildStatus(
status: String,
message: String,
indexedCapsules: Int = rebuildStatus.indexedCapsules
): SemanticRetrievalBuildStatus {
val finished = rebuildStatus.copy(
running = false,
status = status,
finishedAtEpochMs = Instant.now().toEpochMilli(),
message = message,
step = status,
indexedCapsules = indexedCapsules
)
rebuildStatus = finished
// The index/meta just changed, so cached statuses (staleness, doc counts, sizes) are now
// out of date.
invalidateStatusCaches()
broadcastWelcomeEvent("semantic-search-rebuild")
return finished
}
private fun runRebuild(startedAtEpochMs: Long): SemanticRetrievalBuildResult {
val started = if (startedAtEpochMs > 0) startedAtEpochMs else System.currentTimeMillis()
return try {
if (!currentConfig().enabled) {
val status = computeStatus("Semantic search is off.")
val finished = finishRebuildStatus("failed", status.message)
return SemanticRetrievalBuildResult(false, status.message, status, finished)
}
lprintln("info,semantic-search", "semantic retrieval rebuild started")
updateRebuildStatus("collecting", "Collecting semantic search capsules...")
val documentsAtStart = workspaceFingerprintSignature()
val build = buildCapsules()
val capsules = build.capsules
updateRebuildStatus("embedding", "Preparing ${capsules.size} semantic search capsule(s)...", capsules.size)
val currentEmbedder = loadEmbedder()
val path = indexPath()
path.createDirectories()
FSDirectory.open(path).use { directory ->
val config = IndexWriterConfig(indexAnalyzer)
.setOpenMode(IndexWriterConfig.OpenMode.CREATE)
.setRAMBufferSizeMB(64.0)
IndexWriter(directory, config).use { writer ->
val batchSize = semanticEmbeddingBatchSize()
updateRebuildStatus(
"embedding",
if (currentEmbedder == null) {
"Writing lexical index for ${capsules.size} capsule(s)..."
} else {
"Embedding ${capsules.size} capsule(s) with ${currentEmbedder.backend} in batches of $batchSize..."
},
capsules.size
)
if (currentEmbedder == null) {
capsules.forEach { capsule ->
writer.updateDocument(Term("id", capsule.id), capsule.toLuceneDocument(null, null))
}
} else {
var indexed = 0
var batchIndex = 0
while (indexed < capsules.size) {
val end = (indexed + batchSize).coerceAtMost(capsules.size)
val batch = capsules.subList(indexed, end)
updateRebuildStatus(
"embedding",
"Embedding semantic search batch ${batchIndex + 1} (${indexed + 1}-${indexed + batch.size} of ${capsules.size})...",
indexed
)
val intentVectors = currentEmbedder.encodeDocuments(batch.map { it.intentText() })
// A capsule with no source (every docs section) has implementationText == intentText:
// embedding it twice doubled the rebuild for an identical vector.
val distinct = batch.indices.filter { batch[it].source.isNotBlank() }
val distinctVectors = if (distinct.isEmpty()) emptyList()
else currentEmbedder.encodeDocuments(distinct.map { batch[it].implementationText() })
val implementationVectors = batch.indices.map { i ->
distinct.indexOf(i).takeIf { it >= 0 }?.let { distinctVectors[it] } ?: intentVectors[i]
}
updateRebuildStatus(
"writing",
"Writing semantic search batch ${batchIndex + 1} (${indexed + 1}-${indexed + batch.size} of ${capsules.size})...",
indexed
)
batch.forEachIndexed { index, capsule ->
val doc = capsule.toLuceneDocument(
intentVector = intentVectors.getOrNull(index),
implementationVector = implementationVectors.getOrNull(index)
)
writer.updateDocument(Term("id", capsule.id), doc)
}
indexed += batch.size
batchIndex += 1
}
}
writer.commit()
}
}
val meta = listOf(
"builtAtEpochMs=$started",
"capsules=${capsules.size}",
"embedder=${currentEmbedder?.backend ?: "disabled"}",
"dimension=${currentEmbedder?.dimension ?: 0}",
"vectorFormat=${if (currentEmbedder != null) VECTOR_FORMAT else ""}",
"capsuleBackend=source-index+workspace",
"workspaceDocuments=${build.workspaceDocuments}",
"workspaceSidecarFiles=${build.workspaceSidecarFiles}",
"corpusFingerprint=${build.corpusFingerprint}",
"workspaceRoot=${workspaceRootPath()?.toString().orEmpty()}"
).joinToString("\n")
Files.writeString(path.resolve(META_FILE), meta, StandardCharsets.UTF_8)
Files.writeString(path.resolve(DOCUMENTS_FILE), documentsAtStart, StandardCharsets.UTF_8)
val status = computeStatus(
"Built semantic retrieval index with ${capsules.size} capsule(s) in ${System.currentTimeMillis() - started}ms."
)
lastStatus = status
val finished = finishRebuildStatus("succeeded", status.message, capsules.size)
lprintln("info,semantic-search", status.message)
SemanticRetrievalBuildResult(true, status.message, status, finished)
} catch (t: Throwable) {
val message = "Semantic retrieval rebuild failed: ${t.message ?: t.javaClass.simpleName}"
lprintln("error,semantic-search", message)
val status = computeStatus(message)
val finished = finishRebuildStatus("failed", message)
SemanticRetrievalBuildResult(false, message, status, finished)
}
}
fun rebuild(): SemanticRetrievalBuildResult
fun rebuild(): SemanticRetrievalBuildResult {
val startedStatus = beginRebuild("Semantic retrieval rebuild started.")
?: return SemanticRetrievalBuildResult(false, rebuildStatus.message, status(), rebuildStatus)
return runRebuild(startedStatus.startedAtEpochMs)
}
fun startRebuild(): SemanticRetrievalBuildResult
fun startRebuild(): SemanticRetrievalBuildResult {
val startedStatus = beginRebuild("Semantic retrieval rebuild queued.")
?: return SemanticRetrievalBuildResult(true, rebuildStatus.message, status(), rebuildStatus)
lprintln("info,semantic-search", "semantic retrieval rebuild queued")
thread(name = "semantic-retrieval-rebuild", isDaemon = true) {
runRebuild(startedStatus.startedAtEpochMs)
}
return SemanticRetrievalBuildResult(true, "Semantic retrieval rebuild started.", status(), rebuildStatus)
}
fun query(request: SemanticRetrievalQueryRequest): SemanticRetrievalQueryResult
body — 139 lines, from line 902
fun query(request: SemanticRetrievalQueryRequest): SemanticRetrievalQueryResult {
if (!currentConfig().enabled) {
return SemanticRetrievalQueryResult(false, "Semantic search is off.", status())
}
val safeQuery = request.query.trim()
if (safeQuery.isBlank()) {
return SemanticRetrievalQueryResult(false, "Query is blank.", status())
}
if (!indexExists() && request.rebuildIfEmpty) {
rebuild()
}
if (!indexExists()) {
return SemanticRetrievalQueryResult(false, "Semantic retrieval index does not exist yet.", status())
}
val limit = request.limit.coerceIn(1, 50)
val currentEmbedder = loadEmbedder()
val requestedCorpora = request.normalizedCorpora()
val requestedKinds = request.kinds.map { it.trim().lowercase(Locale.ROOT) }.filter { it.isNotBlank() }.toSet()
val filter: (Document) -> Boolean = { doc ->
val corpusOk = requestedCorpora.isEmpty() || normalizeCorpusName(doc.get("corpus").orEmpty()) in requestedCorpora
val pathOk = request.pathPrefix.isBlank() || doc.get("path").orEmpty().startsWith(request.pathPrefix)
val kindOk = requestedKinds.isEmpty() || doc.get("kind").orEmpty().lowercase(Locale.ROOT) in requestedKinds
val documentOk = request.documentName.isBlank() ||
doc.get("documentName").orEmpty().equals(request.documentName, ignoreCase = true)
val boxOk = request.boxId.isBlank() || doc.get("boxId").orEmpty() == request.boxId
val propertyOk = request.property.isBlank() ||
doc.get("property").orEmpty().equals(request.property, ignoreCase = true)
corpusOk && pathOk && kindOk && documentOk && boxOk && propertyOk
}
return runCatching {
FSDirectory.open(indexPath()).use { directory ->
DirectoryReader.open(directory).use { reader ->
val searcher = IndexSearcher(reader)
val candidateCount = (limit * 8).coerceAtLeast(80)
// The corpus is filtered inside each query, not after it: filtered afterwards, a
// documentation search kept only the documentation that happened to be in the global top
// 80, which for "display" was none of it (2026-10-22). The other filters stay post-hoc.
val corpusFilter = corpusFilterQuery(requestedCorpora)
fun within(query: Query): Query = corpusFilter?.let {
BooleanQuery.Builder().add(query, BooleanClause.Occur.MUST).add(it, BooleanClause.Occur.FILTER).build()
} ?: query
val fusion = evalSetting("fusion") ?: "hybrid"
val bm25 = if (fusion == "dense") emptyList() else searcher.search(within(buildBm25Query(safeQuery)), candidateCount)
.scoreDocs
.toRanked(searcher, filter)
val symbol = if (fusion == "dense") emptyList() else searcher.search(within(buildSymbolQuery(safeQuery)), candidateCount)
.scoreDocs
.toRanked(searcher, filter)
// Stale zero vectors would only add the same arbitrary neighbours to every query.
val vectorsUsable = !vectorsOutdated(readMeta()) && fusion != "lexical"
val vector = currentEmbedder?.takeIf { vectorsUsable }?.encodeQueries(listOf(safeQuery))?.firstOrNull()?.let { queryVector ->
val intentHits = searcher.search(KnnFloatVectorQuery(INTENT_VECTOR, queryVector, candidateCount, corpusFilter), candidateCount)
.scoreDocs
.toRanked(searcher, filter)
val implementationHits = searcher.search(
KnnFloatVectorQuery(IMPLEMENTATION_VECTOR, queryVector, candidateCount, corpusFilter),
candidateCount
).scoreDocs.toRanked(searcher, filter, scoreScale = 0.85f)
mergeVectorRanks(intentHits, implementationHits)
}.orEmpty()
val fused = fuseRanks(searcher, vector, bm25, symbol, limit, request)
val scope = if (requestedCorpora.isEmpty()) {
"all corpora"
} else {
requestedCorpora.joinToString(",")
}
val currentStatus = status()
SemanticRetrievalQueryResult(
success = true,
message = staleWarning(currentStatus) +
"Returned ${fused.size} semantic retrieval hit(s) from $scope. Vector backend=${currentEmbedder?.backend ?: "disabled"}" +
(if (!vectorsUsable) " (not used: the index's vectors predate the embedding fix; rebuild the index)" else "") +
"; capsule backend=source-index-capsules.",
status = currentStatus,
results = fused
)
}
}
}.getOrElse {
SemanticRetrievalQueryResult(false, it.message ?: it.javaClass.name, status())
}
}
private fun loadEmbedder(): SemanticEmbedder? {
if (!currentConfig().enabled) return null
val current = embedder
if (current != null) return current
val config = OnnxSemanticEmbedder.Config.fromSettings(currentConfig())
val created = config.createOrNull()
embedder = created
embedderMessage = created?.backend ?: OnnxSemanticEmbedder.Config.disabledReason(config)
return created
}
private fun computeStatus(message: String): SemanticRetrievalStatus {
val config = currentConfig()
val meta = readMeta()
val exists = indexExists()
// The fingerprint is a full-workspace scan (parses every document's metadata). It only
// feeds the "index stale?" comparison and the displayed current fingerprint, both of which
// are meaningless until an index has actually been built — so skip the scan entirely when
// no index exists (the common case on the welcome screen).
val currentFingerprint = if (exists) currentCorpusFingerprint() else ""
val indexedFingerprint = meta["corpusFingerprint"].orEmpty()
val documents = if (exists) {
runCatching {
FSDirectory.open(indexPath()).use { directory ->
DirectoryReader.open(directory).use { it.numDocs() }
}
}.getOrDefault(0)
} else {
0
}
val status = SemanticRetrievalStatus(
enabled = config.enabled,
indexPath = indexPath().absolutePathString(),
indexExists = exists,
indexedDocuments = documents,
indexedCapsules = meta["capsules"]?.toIntOrNull() ?: documents,
capsuleBackend = displayCorpusName(meta["capsuleBackend"] ?: "source-index+workspace"),
embedderBackend = if (!config.enabled) "disabled" else embedder?.backend ?: meta["embedder"] ?: "disabled",
embeddingDimension = embedder?.dimension ?: meta["dimension"]?.toIntOrNull() ?: 0,
lastBuildEpochMs = meta["builtAtEpochMs"]?.toLongOrNull() ?: 0L,
workspaceRoot = workspaceRootPath()?.toString().orEmpty(),
workspaceDocuments = meta["workspaceDocuments"]?.toIntOrNull() ?: 0,
workspaceSidecarFiles = meta["workspaceSidecarFiles"]?.toIntOrNull() ?: 0,
corpusFingerprint = indexedFingerprint,
currentCorpusFingerprint = currentFingerprint,
indexStale = exists && (vectorsOutdated(meta) || currentFingerprint.isNotBlank() &&
(indexedFingerprint.isBlank() || indexedFingerprint != currentFingerprint)),
staleDocuments = if (exists) staleDocumentCount() else -1,
message = message.ifBlank { if (config.enabled) embedderMessage else "Semantic search is off." }
)
lastStatus = status
return status
}
internal fun staleWarning(status: SemanticRetrievalStatus): String
A stale index misses whatever changed since it was built, so a miss is not evidence of absence. This leads the message because a model reads the message and skips the status block; a local model searching for a docs page written that morning reported "zero hits anywhere" on 2026-10-28.
internal fun staleWarning(status: SemanticRetrievalStatus): String {
if (!status.indexStale) return ""
val built = if (status.lastBuildEpochMs > 0) " built ${java.time.Instant.ofEpochMilli(status.lastBuildEpochMs)}" else ""
return "WARNING: the index$built is stale, so pages, source and documents written or changed since then are missing " +
"from these results; a miss here is not evidence that something does not exist. Confirm with a literal " +
"WorkspaceSearchRequest (scope FIELD_DOCS for docs pages, FIELD_KOTLIN_SOURCE for source, WORKSPACE_FILES for documents). "
}
private fun staleDocumentCount(): Int {
val file = indexPath().resolve(DOCUMENTS_FILE)
if (!Files.isRegularFile(file)) return -1
val built = runCatching { Files.readAllLines(file, StandardCharsets.UTF_8) }.getOrNull() ?: return -1
return changedDocumentNames(built, workspaceFingerprintSignature().lines()).size
}
internal fun changedDocumentNames(before: List<String>, after: List<String>): Set<String>
Names of documents whose name:mtime:size line differs between two signatures, either way round.
internal fun changedDocumentNames(before: List<String>, after: List<String>): Set<String> {
val a = before.filter { it.isNotBlank() }.toSet()
val b = after.filter { it.isNotBlank() }.toSet()
return ((a - b) + (b - a)).mapTo(sortedSetOf()) { it.substringBeforeLast(':').substringBeforeLast(':') }
}
idle rebuild: keep a stale index current without anyone pressing "Rebuild index"
private const val IDLE_CHECK_MS = 10L * 60_000L
private const val IDLE_QUIET_MS = 30L * 60_000L // no document saved for this long
private const val IDLE_MIN_INTERVAL_MS = 6L * 3_600_000L // and the last build is at least this old
@Volatile private var idleWatcherStarted = false
fun startIdleRebuildWatcher()
Starts a daemon that rebuilds a stale index once the workspace has been quiet for
IDLE_QUIET_MS. It only touches an index this workspace built (the meta's workspaceRoot),
since the index is shared machine-wide and an automation server has a workspace of its own.
AFIELD_SEMANTIC_IDLE_REBUILD=0 turns it off.
fun startIdleRebuildWatcher() {
if (idleWatcherStarted) return
idleWatcherStarted = true
val setting = firstNonBlank(System.getProperty("afield.semanticSearch.idleRebuild"), System.getenv("AFIELD_SEMANTIC_IDLE_REBUILD"))
if (setting == "0" || setting.equals("false", ignoreCase = true)) return
thread(name = "semantic-retrieval-idle-rebuild", isDaemon = true, priority = Thread.MIN_PRIORITY) {
while (true) {
Thread.sleep(IDLE_CHECK_MS)
runCatching { if (idleRebuildDue()) startRebuild() }
.onFailure { lprintln("warning,semantic-search", "idle rebuild check failed: ${it.message ?: it.javaClass.simpleName}") }
}
}
}
private fun idleRebuildDue(): Boolean {
if (!currentConfig().enabled || rebuildStatus.running) return false
if (runCatching { currentFieldPreferences().quietMode }.getOrDefault(false)) return false
if (!indexExists()) return false
val meta = readMeta()
val root = workspaceRootPath()?.toString() ?: return false
if (meta["workspaceRoot"] != root) return false
val now = System.currentTimeMillis()
val builtAt = meta["builtAtEpochMs"]?.toLongOrNull() ?: 0L
if (now - builtAt < IDLE_MIN_INTERVAL_MS) return false
val lastSave = workspaceFingerprintSignature().lines()
.mapNotNull { it.substringBeforeLast(':').substringAfterLast(':').toLongOrNull() }
.maxOrNull() ?: return false
if (now - lastSave < IDLE_QUIET_MS) return false
return (vectorsOutdated(meta) || staleDocumentCount() != 0) && computeStatus("").indexStale
}
An index whose vectors predate VECTOR_FORMAT while an embedder is configured: its vectors are zero.
source — 62 lines, from line 1113
private fun vectorsOutdated(meta: Map<String, String>): Boolean =
currentConfig().enabled && meta.isNotEmpty() && meta["embedder"].orEmpty().let { it.isNotBlank() && it != "disabled" } &&
meta["vectorFormat"] != VECTOR_FORMAT
private fun indexExists(): Boolean =
Files.exists(indexPath()) && runCatching {
FSDirectory.open(indexPath()).use { DirectoryReader.indexExists(it) }
}.getOrDefault(false)
private fun readMeta(): Map<String, String> {
val file = indexPath().resolve(META_FILE)
if (!Files.exists(file)) return emptyMap()
return runCatching {
Files.readAllLines(file, StandardCharsets.UTF_8)
.mapNotNull { line ->
val idx = line.indexOf('=')
if (idx <= 0) null else line.substring(0, idx) to line.substring(idx + 1)
}
.toMap()
}.getOrDefault(emptyMap())
}
private fun indexPath(): Path = explicitIndexPath ?: cacheRoot.resolve("lucene").toAbsolutePath().normalize()
private fun currentConfig(): SemanticRetrievalConfig {
val fromFile = if (Files.exists(configPath)) {
runCatching { json.decodeFromString<SemanticRetrievalConfig>(Files.readString(configPath, StandardCharsets.UTF_8)) }
.getOrNull()
} else {
null
} ?: SemanticRetrievalConfig()
val envModel = firstNonBlank(System.getProperty("afield.semanticSearch.onnx.model"), System.getenv("AFIELD_SEMANTIC_SEARCH_ONNX_MODEL"))
val envTokenizer = firstNonBlank(System.getProperty("afield.semanticSearch.onnx.tokenizer"), System.getenv("AFIELD_SEMANTIC_SEARCH_ONNX_TOKENIZER"))
val envDimension = firstNonBlank(System.getProperty("afield.semanticSearch.dimension"), System.getenv("AFIELD_SEMANTIC_SEARCH_DIMENSION"))
?.toIntOrNull()
val envMaxTokens = firstNonBlank(System.getProperty("afield.semanticSearch.maxTokens"), System.getenv("AFIELD_SEMANTIC_SEARCH_MAX_TOKENS"))
?.toIntOrNull()
val envQueryPrefix = firstNonBlank(System.getProperty("afield.semanticSearch.queryPrefix"), System.getenv("AFIELD_SEMANTIC_SEARCH_QUERY_PREFIX"))
val envDocumentPrefix = firstNonBlank(System.getProperty("afield.semanticSearch.documentPrefix"), System.getenv("AFIELD_SEMANTIC_SEARCH_DOCUMENT_PREFIX"))
return fromFile.copy(
modelPath = envModel ?: fromFile.modelPath,
tokenizerPath = envTokenizer ?: fromFile.tokenizerPath,
dimension = envDimension?.coerceIn(1, 1024) ?: fromFile.dimension,
maxTokens = envMaxTokens?.coerceIn(16, 8192) ?: fromFile.maxTokens,
queryPrefix = envQueryPrefix ?: fromFile.queryPrefix,
documentPrefix = envDocumentPrefix ?: fromFile.documentPrefix
)
}
private fun persistConfig(config: SemanticRetrievalConfig) {
configPath.parent.createDirectories()
Files.writeString(configPath, json.encodeToString(config), StandardCharsets.UTF_8)
}
private fun resetEmbedder() {
embedder?.close()
embedder = null
embedderMessage = ""
invalidateStatusCaches()
}
Drop every memoized status so the next read reflects a config/index/workspace change.
source — 396 lines, from line 1177
private fun invalidateStatusCaches() {
lastStatus = null
settingsStatusCache = null
fingerprintCache = null
}
private fun semanticEmbeddingBatchSize(): Int =
semanticSearchBatchSizeSetting()
private fun firstNonBlank(vararg values: String?): String? =
values.firstOrNull { !it.isNullOrBlank() }?.trim()
private fun downloadFile(url: String, target: Path) {
target.parent?.createDirectories()
val tmp = target.resolveSibling("${target.fileName}.part")
val request = HttpRequest.newBuilder(URI.create(url))
.GET()
.header("User-Agent", "Field4 semantic-search")
.build()
val response = downloadClient.send(request, HttpResponse.BodyHandlers.ofFile(tmp))
if (response.statusCode() !in 200..299) {
runCatching { Files.deleteIfExists(tmp) }
error("Download failed for $url: HTTP ${response.statusCode()}")
}
Files.move(tmp, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING, java.nio.file.StandardCopyOption.ATOMIC_MOVE)
}
private fun fileState(rawPath: String): SemanticRetrievalFileState {
if (rawPath.isBlank()) return SemanticRetrievalFileState()
val path = Paths.get(rawPath).toAbsolutePath().normalize()
val exists = Files.exists(path)
return SemanticRetrievalFileState(
path = path.absolutePathString(),
exists = exists,
sizeBytes = if (exists && Files.isRegularFile(path)) Files.size(path) else 0L,
lastModifiedEpochMs = if (exists) runCatching { Files.getLastModifiedTime(path).toMillis() }.getOrDefault(0L) else 0L
)
}
private fun directorySize(path: Path): Long {
if (!Files.exists(path)) return 0L
return runCatching {
Files.walk(path).use { stream ->
stream.filter { Files.isRegularFile(it) }.mapToLong { Files.size(it) }.sum()
}
}.getOrDefault(0L)
}
private fun deleteRecursively(path: Path) {
if (!Files.exists(path)) return
Files.walk(path).use { stream ->
stream.sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) }
}
}
private data class CapsuleBuild(
val capsules: List<SemanticCapsule>,
val workspaceDocuments: Int,
val workspaceSidecarFiles: Int,
val corpusFingerprint: String
)
private data class WorkspaceSidecarRecord(
val documentName: String,
val documentDir: Path,
val metadataMtime: Long,
val boxId: String,
val boxName: String,
val property: String,
val language: String,
val file: Path,
val relativePath: String,
val sizeBytes: Long,
val modifiedEpochMs: Long
)
private data class WorkspaceSourceFragment(
val scale: String,
val kind: String,
val symbol: String,
val title: String,
val line: Int,
val endLine: Int,
val signature: String,
val metadata: String = "",
val source: String
)
private fun buildCapsules(): CapsuleBuild {
val root = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize()
val index = KotlinSourceIndex.latestIndex ?: KotlinSourceIndex.loadIndex()
val kotlinCapsules = sourceCapsules(root, index)
val docsCapsules = docsCapsules(root)
val workspaceRecords = workspaceSidecarRecords()
val workspaceCapsules = workspaceSidecarCapsules(workspaceRecords)
val tutorialCapsules = workspaceSidecarCapsules(sidecarRecords(root, tutorialDocumentDirs(root)), corpus = "tutorials") +
tutorialSummaryCapsules(root)
val allLeafCapsules = kotlinCapsules + docsCapsules + workspaceCapsules + tutorialCapsules
val subsystemCapsules = subsystemCapsules(allLeafCapsules)
val repositoryCapsule = SemanticCapsule(
id = "repo:field4",
scale = "repository",
corpus = "field-internals",
kind = "repository",
symbol = "Field4",
title = "Field4 repository",
path = ".",
line = 1,
metadata = "Indexed at ${Instant.now()}; declarations=${kotlinCapsules.size}; documentation_sections=${docsCapsules.size}; workspace_sidecars=${workspaceCapsules.size}."
)
val onlyCorpora = evalSetting("corpora")?.split(',')?.map { normalizeCorpusName(it) }?.toSet()
return CapsuleBuild(
capsules = (allLeafCapsules + subsystemCapsules + repositoryCapsule).filter { onlyCorpora == null || normalizeCorpusName(it.corpus) in onlyCorpora },
workspaceDocuments = workspaceRecords.map { it.documentName }.toSet().size,
workspaceSidecarFiles = workspaceRecords.size,
corpusFingerprint = corpusFingerprint(workspaceRecords)
)
}
private fun workspaceSidecarCapsules(records: List<WorkspaceSidecarRecord>, corpus: String = "workspace"): List<SemanticCapsule> {
return records.flatMap { record ->
val source = runCatching { Files.readString(record.file, StandardCharsets.UTF_8) }.getOrNull()
?.take(80_000)
?: return@flatMap emptyList()
if (source.isBlank()) return@flatMap emptyList()
val firstLine = source.lineSequence().firstOrNull { it.trim().isNotBlank() }?.trim().orEmpty()
val hash = source.sha256Id()
val lineCount = source.count { it == '\n' } + 1
val propertyId = "$corpus:${record.documentName}:${record.boxId}:${record.property}:$hash".sha256Id()
val symbol = listOf(record.documentName, record.boxName.ifBlank { record.boxId }, record.property)
.filter { it.isNotBlank() }
.joinToString(" / ")
val propertyCapsule = SemanticCapsule(
id = propertyId,
scale = "container",
corpus = corpus,
kind = "sidecar-${record.property}",
symbol = symbol,
title = "${record.boxName.ifBlank { record.boxId }}.${record.property}",
path = record.relativePath,
line = 1,
endLine = lineCount,
parentId = "workspace-box:${record.documentName}:${record.boxId}",
documentName = record.documentName,
boxName = record.boxName,
boxId = record.boxId,
property = record.property,
language = record.language,
signature = firstLine,
metadata = buildString {
appendLine("document: ${record.documentName}")
appendLine("box: ${record.boxName}")
appendLine("box_uid: ${record.boxId}")
appendLine("property: ${record.property}")
appendLine("language: ${record.language}")
appendLine("file_hash: $hash")
appendLine("byte_size: ${record.sizeBytes}")
appendLine("modified_epoch_ms: ${record.modifiedEpochMs}")
}.trim(),
source = source
)
val fragments = workspaceSourceFragments(record, source)
.mapIndexed { index, fragment ->
SemanticCapsule(
id = "workspace-fragment:$propertyId:${fragment.kind}:${fragment.line}:${fragment.symbol}:$index".sha256Id(),
scale = fragment.scale,
corpus = corpus,
kind = fragment.kind,
symbol = fragment.symbol.ifBlank { symbol },
title = fragment.title.ifBlank { "${record.boxName.ifBlank { record.boxId }}.${record.property}" },
path = record.relativePath,
line = fragment.line,
endLine = fragment.endLine,
parentId = propertyId,
documentName = record.documentName,
boxName = record.boxName,
boxId = record.boxId,
property = record.property,
language = record.language,
signature = fragment.signature,
metadata = buildString {
appendLine("document: ${record.documentName}")
appendLine("box: ${record.boxName}")
appendLine("box_uid: ${record.boxId}")
appendLine("property: ${record.property}")
appendLine("language: ${record.language}")
appendLine("parent_property_id: $propertyId")
if (fragment.metadata.isNotBlank()) appendLine(fragment.metadata)
}.trim(),
source = fragment.source
)
}
listOf(propertyCapsule) + fragments
}
}
private fun workspaceSourceFragments(record: WorkspaceSidecarRecord, source: String): List<WorkspaceSourceFragment> {
val language = record.language.lowercase(Locale.ROOT)
val structured = when {
language in setOf("kotlin", "kt", "kts") -> kotlinWorkspaceFragments(source)
language in setOf("python", "py") -> pythonWorkspaceFragments(source)
language in setOf("javascript", "typescript", "js", "ts") -> javascriptWorkspaceFragments(source)
language in setOf("glsl", "slang", "stacksl") -> shaderWorkspaceFragments(source)
language in setOf("markdown", "md") -> markdownWorkspaceFragments(source)
else -> emptyList()
}
return (structured.ifEmpty { fallbackWorkspaceFragments(source) }).take(64)
}
private fun kotlinWorkspaceFragments(source: String): List<WorkspaceSourceFragment> {
val lines = source.lines()
val decl = Regex("""^\s*(?:[\w\s@().<>"]+\s+)?(fun|val|var|class|interface|object)\s+([A-Za-z_][A-Za-z0-9_.]*)""")
return lines.mapIndexedNotNull { index, line ->
if (line.leadingWhitespace() > 4) return@mapIndexedNotNull null
val match = decl.find(line) ?: return@mapIndexedNotNull null
val kind = match.groupValues[1]
val symbol = match.groupValues[2]
val body = declarationSource(lines, index, maxLines = 180).ifBlank { line.trim() }
val calls = extractCallNames(body).take(24)
WorkspaceSourceFragment(
scale = "declaration",
kind = kind,
symbol = symbol,
title = symbol.substringAfterLast('.'),
line = index + 1,
endLine = index + body.lines().size,
signature = line.trim(),
metadata = if (calls.isEmpty()) "" else buildString {
appendLine("calls:")
calls.forEach { appendLine("- $it") }
}.trim(),
source = body
)
}
}
private fun pythonWorkspaceFragments(source: String): List<WorkspaceSourceFragment> {
val lines = source.lines()
val decl = Regex("""^\s*((?:async\s+)?def|class)\s+([A-Za-z_][A-Za-z0-9_]*)\b""")
return lines.mapIndexedNotNull { index, line ->
if (line.leadingWhitespace() > 4) return@mapIndexedNotNull null
val match = decl.find(line) ?: return@mapIndexedNotNull null
val keyword = match.groupValues[1]
val symbol = match.groupValues[2]
val body = indentationBlockSource(lines, index, maxLines = 180).ifBlank { line.trim() }
WorkspaceSourceFragment(
scale = "declaration",
kind = if (keyword.contains("def")) "function" else "class",
symbol = symbol,
title = symbol,
line = index + 1,
endLine = index + body.lines().size,
signature = line.trim(),
source = body
)
}
}
private fun javascriptWorkspaceFragments(source: String): List<WorkspaceSourceFragment> {
val lines = source.lines()
val functionDecl = Regex("""^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\b""")
val classDecl = Regex("""^\s*(?:export\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)\b""")
val arrowDecl = Regex("""^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>""")
return lines.mapIndexedNotNull { index, line ->
if (line.leadingWhitespace() > 4) return@mapIndexedNotNull null
val kind: String
val symbol: String
when {
functionDecl.find(line) != null -> {
kind = "function"
symbol = functionDecl.find(line)!!.groupValues[1]
}
classDecl.find(line) != null -> {
kind = "class"
symbol = classDecl.find(line)!!.groupValues[1]
}
arrowDecl.find(line) != null -> {
kind = "function"
symbol = arrowDecl.find(line)!!.groupValues[1]
}
else -> return@mapIndexedNotNull null
}
val body = declarationSource(lines, index, maxLines = 180).ifBlank { line.trim() }
WorkspaceSourceFragment(
scale = "declaration",
kind = kind,
symbol = symbol,
title = symbol,
line = index + 1,
endLine = index + body.lines().size,
signature = line.trim(),
source = body
)
}
}
private fun shaderWorkspaceFragments(source: String): List<WorkspaceSourceFragment> {
val lines = source.lines()
val structDecl = Regex("""^\s*struct\s+([A-Za-z_][A-Za-z0-9_]*)\b""")
val functionDecl = Regex("""^\s*(?:[A-Za-z_][A-Za-z0-9_<>]*\s+){1,5}([A-Za-z_][A-Za-z0-9_]*)\s*\([^;]*\)\s*(?:\{|$)""")
val controlWords = setOf("if", "for", "while", "switch")
return lines.mapIndexedNotNull { index, line ->
if (line.leadingWhitespace() > 4) return@mapIndexedNotNull null
val struct = structDecl.find(line)
val function = functionDecl.find(line)
val kind: String
val symbol: String
when {
struct != null -> {
kind = "struct"
symbol = struct.groupValues[1]
}
function != null && function.groupValues[1] !in controlWords -> {
kind = "shader-function"
symbol = function.groupValues[1]
}
else -> return@mapIndexedNotNull null
}
val body = declarationSource(lines, index, maxLines = 180).ifBlank { line.trim() }
WorkspaceSourceFragment(
scale = "declaration",
kind = kind,
symbol = symbol,
title = symbol,
line = index + 1,
endLine = index + body.lines().size,
signature = line.trim(),
source = body
)
}
}
private fun markdownWorkspaceFragments(source: String): List<WorkspaceSourceFragment> =
markdownSections(source.lines()).map { section ->
WorkspaceSourceFragment(
scale = "fragment",
kind = "markdown-section",
symbol = section.title,
title = section.title,
line = section.line,
endLine = section.endLine,
signature = section.title,
metadata = "markdown_heading_level: ${section.level}",
source = section.body
)
}
private fun fallbackWorkspaceFragments(source: String): List<WorkspaceSourceFragment> {
val lines = source.lines()
if (lines.size <= 180 && source.length <= 16_000) return emptyList()
val chunkSize = 120
val overlap = 20
val step = chunkSize - overlap
val out = mutableListOf<WorkspaceSourceFragment>()
var start = 0
while (start < lines.size && out.size < 16) {
val endExclusive = (start + chunkSize).coerceAtMost(lines.size)
val chunk = lines.subList(start, endExclusive).joinToString("\n").trim()
if (chunk.isNotBlank()) {
val firstLine = chunk.lineSequence().firstOrNull { it.trim().isNotBlank() }?.trim().orEmpty()
out += WorkspaceSourceFragment(
scale = "fragment",
kind = "text-fragment",
symbol = "lines ${start + 1}-$endExclusive",
title = "lines ${start + 1}-$endExclusive",
line = start + 1,
endLine = endExclusive,
signature = firstLine,
metadata = "chunk_lines: ${start + 1}-$endExclusive",
source = chunk
)
}
start += step
}
return out
}
private fun indentationBlockSource(lines: List<String>, start: Int, maxLines: Int = 180): String {
val baseIndent = lines[start].leadingWhitespace()
val out = mutableListOf<String>()
for (idx in start until (start + maxLines).coerceAtMost(lines.size)) {
val line = lines[idx]
if (idx > start && line.isNotBlank() && line.leadingWhitespace() <= baseIndent) break
out += line
}
return out.joinToString("\n").trimEnd()
}
private fun String.leadingWhitespace(): Int =
takeWhile { it == ' ' || it == '\t' }.sumOf { if (it == '\t') 4 else 1 }
private fun workspaceSidecarRecords(): List<WorkspaceSidecarRecord> {
val root = workspaceRootPath() ?: return emptyList()
if (!Files.isDirectory(root)) return emptyList()
return sidecarRecords(root, workspaceDocumentDirs(root))
}
internal fun tutorialDocumentDirs(checkout: Path): List<Path>
Field's tutorial documents: each materialized agent transcript (public/docs/assets/agent_transcripts/<name>)
and the graphics curriculum (public/docs/assets/canonical_graphics). They are Field documents like a
workspace's, so they are indexed the same way, box by box, with paths relative to the checkout so a hit can
be read with AttachmentRequest. TutorialCodeSearchRequest is a query on this corpus.
internal fun tutorialDocumentDirs(checkout: Path): List<Path> {
val assets = checkout.resolve("public/docs/assets")
val transcripts = assets.resolve("agent_transcripts")
val dirs = runCatching { Files.list(transcripts).use { s -> s.filter { Files.isRegularFile(it.resolve(SavingContext.METADATA_EDN)) }.sorted().toList() } }
.getOrDefault(emptyList())
return dirs + listOf(assets.resolve("canonical_graphics")).filter { Files.isRegularFile(it.resolve(SavingContext.METADATA_EDN)) }
}
What each tutorial is for, beside its code: the transcript's description and box names for each materialized
transcript, and for the graphics curriculum one capsule per lesson from its generated index (LESSONS.md:
title, parent chain, the Field API it uses). Indexed only as code, "FLines" did not find fline_measure.
The transcript's docs links are left out: they are what RetrievalEval's tutorial cases are made from.
source — 184 lines, from line 1594
private fun tutorialSummaryCapsules(checkout: Path): List<SemanticCapsule> {
val transcripts = checkout.resolve("dev_docs/agents/transcripts")
val out = mutableListOf<SemanticCapsule>()
tutorialDocumentDirs(checkout).forEach { dir ->
val name = dir.fileName.toString()
val rel = checkout.relativize(dir).toString().replace("\\", "/")
if (name == "canonical_graphics") {
RetrievalEval.graphicsLessons(dir.resolve("LESSONS.md").toFile()).forEach { lesson ->
out += SemanticCapsule(
id = "tutorial:$name:${lesson.title}".sha256Id(), scale = "container", corpus = "tutorials", kind = "tutorial",
symbol = "graphics lesson ${lesson.title}", title = lesson.title,
path = lesson.files.firstOrNull()?.let { "$rel/$it" } ?: "$rel/LESSONS.md", line = 1,
documentName = name, boxName = lesson.title, boxId = lesson.files.firstOrNull()?.substringBefore('_').orEmpty(),
documentation = lesson.summary)
}
return@forEach
}
val yaml = transcripts.resolve("$name.yaml").toFile().takeIf { it.isFile } ?: return@forEach
val parsed = runCatching { org.yaml.snakeyaml.Yaml().load<Any?>(yaml.readText()) as? Map<*, *> }.getOrNull().orEmpty()
val boxes = (parsed["operations"] as? List<*>).orEmpty().mapNotNull { op ->
((op as? Map<*, *>)?.get("create") as? Map<*, *>)?.get("name")?.toString()
}
out += SemanticCapsule(
id = "tutorial:$name".sha256Id(), scale = "container", corpus = "tutorials", kind = "tutorial",
symbol = name.replace('_', ' '), title = name, path = "$rel/", line = 1, documentName = name,
documentation = parsed["description"]?.toString()?.trim().orEmpty() +
if (boxes.isEmpty()) "" else "\n\nBoxes: " + boxes.joinToString(", "))
}
return out
}
private fun sidecarRecords(root: Path, documentDirs: List<Path>): List<WorkspaceSidecarRecord> {
val context = SavingContext()
val out = mutableListOf<WorkspaceSidecarRecord>()
documentDirs.forEach { documentDir ->
val metadataFile = documentDir.resolve(SavingContext.METADATA_EDN)
val documentName = documentDir.fileName?.toString().orEmpty()
val metadataMtime = runCatching { Files.getLastModifiedTime(metadataFile).toMillis() }.getOrDefault(0L)
val transfers = runCatching { context.readEdnTransfers(documentDir.toString()) }.getOrDefault(emptyList())
transfers.forEach { transfer ->
if (transfer.id == "root") return@forEach
if (transfer.properties["_tombstone"].isTrueLike()) return@forEach
val boxName = transfer.properties["name"]?.toString().orEmpty()
for ((property, extension) in Box.ioSeparateProperties) {
if (!transfer.properties.containsKey(property)) continue
val sidecar = documentDir.resolve("${transfer.id}_${property}_.$extension").normalize()
if (!Files.isRegularFile(sidecar)) continue
val relative = root.relativize(sidecar).toString().replace("\\", "/")
out += WorkspaceSidecarRecord(
documentName = documentName,
documentDir = documentDir,
metadataMtime = metadataMtime,
boxId = transfer.id,
boxName = boxName,
property = property,
language = sidecarLanguage(property, extension, transfer.properties),
file = sidecar,
relativePath = relative,
sizeBytes = runCatching { Files.size(sidecar) }.getOrDefault(0L),
modifiedEpochMs = runCatching { Files.getLastModifiedTime(sidecar).toMillis() }.getOrDefault(0L)
)
}
// File-proxy boxes keep their editable text in the linked file, not in a Field
// sidecar. Index that same file so a presentation attached to a document joins
// the normal workspace semantic-search corpus and stays fresh after edits.
val proxyMode = transfer.properties["file_proxy_mode"] as? String
val proxyPath = transfer.properties["file_proxy_path"] as? String
if (proxyMode != null && !proxyPath.isNullOrBlank()) {
val source = runCatching {
val candidate = when (proxyMode) {
"absolute" -> Paths.get(proxyPath)
"workspace" -> root.resolve(proxyPath)
"document" -> documentDir.resolve(proxyPath)
else -> return@runCatching null
}.toRealPath()
val boundary = when (proxyMode) {
"workspace" -> root.toRealPath()
"document" -> documentDir.toRealPath()
else -> null
}
if (boundary != null && !candidate.startsWith(boundary)) null else candidate
}.getOrNull()
if (source != null && Files.isRegularFile(source)) {
val size = runCatching { Files.size(source) }.getOrDefault(0L)
if (size in 1..10_000_000) {
val extension = source.fileName.toString().substringAfterLast('.', "").lowercase(Locale.ROOT)
val language = (transfer.properties["contents_language"] as? String)
?.lowercase(Locale.ROOT)
?: when (extension) {
"ts", "tsx" -> "typescript"
"js", "jsx", "mjs" -> "javascript"
"md" -> "markdown"
else -> extension
}
out += WorkspaceSidecarRecord(
documentName = documentName,
documentDir = documentDir,
metadataMtime = metadataMtime,
boxId = transfer.id,
boxName = boxName,
property = "contents",
language = language,
file = source,
relativePath = if (source.startsWith(root)) root.relativize(source).toString().replace("\\", "/") else source.toString(),
sizeBytes = size,
modifiedEpochMs = Files.getLastModifiedTime(source).toMillis()
)
}
}
}
}
}
return out.sortedWith(compareBy<WorkspaceSidecarRecord> { it.documentName }.thenBy { it.boxId }.thenBy { it.property })
}
private fun workspaceDocumentDirs(root: Path): List<Path> {
return runCatching {
Files.list(root).use { stream ->
stream
.filter { Files.isDirectory(it) }
.filter { path ->
val name = path.fileName?.toString().orEmpty()
name.isNotBlank() &&
!name.startsWith(".") &&
name != "_projects" &&
!name.equals("backups", ignoreCase = true) &&
!name.equals("_backups", ignoreCase = true) &&
Files.isRegularFile(path.resolve(SavingContext.METADATA_EDN))
}
.sorted()
.toList()
}
}.getOrDefault(emptyList())
}
private fun workspaceRootPath(): Path? {
val configured = firstNonBlank(
System.getProperty("afield.semanticSearch.workspaceRoot"),
System.getenv("AFIELD_SEMANTIC_SEARCH_WORKSPACE_ROOT")
)
return runCatching {
Paths.get(configured ?: home).toAbsolutePath().normalize()
}.getOrNull()
}
private fun sidecarLanguage(property: String, extension: String, properties: Map<String, Any?>): String {
if (property == "code") {
properties["code_language"]?.toString()?.trim()?.takeIf { it.isNotBlank() }?.let {
return it.lowercase(Locale.ROOT)
}
properties["mainLanguage"]?.toString()?.trim()?.takeIf { it.isNotBlank() }?.let {
return it.lowercase(Locale.ROOT)
}
}
return when (extension.lowercase(Locale.ROOT)) {
"kt", "kts" -> "kotlin"
"glsl" -> "glsl"
"slang" -> "slang"
"stacksl" -> "stacksl"
"js" -> "javascript"
"py" -> "python"
"md" -> "markdown"
"yaml", "yml" -> "yaml"
else -> extension.lowercase(Locale.ROOT)
}
}
private fun corpusFingerprint(records: List<WorkspaceSidecarRecord>): String {
val payload = buildString {
records.forEach { record ->
append(record.relativePath).append('\t')
append(record.documentName).append('\t')
append(record.boxId).append('\t')
append(record.boxName).append('\t')
append(record.property).append('\t')
append(record.language).append('\t')
append(record.sizeBytes).append('\t')
append(record.modifiedEpochMs).append('\t')
append(record.metadataMtime).append('\n')
}
}
return payload.sha256Id()
}
Memoize the (expensive) corpus fingerprint against a cheap signature built purely from per-document metadata.edn mtime+size — no EDN parsing. Saving a document rewrites its metadata.edn, so this signature changes whenever content that could affect the fingerprint changes, and we only pay the full scan again after a real edit.
source — 158 lines, from line 1783
@Volatile
private var fingerprintCache: Pair<String, String>? = null
private fun currentCorpusFingerprint(): String {
val signature = workspaceFingerprintSignature()
fingerprintCache?.let { if (it.first == signature) return it.second }
val fingerprint = corpusFingerprint(workspaceSidecarRecords())
fingerprintCache = signature to fingerprint
return fingerprint
}
private fun workspaceFingerprintSignature(): String {
val root = workspaceRootPath() ?: return ""
if (!Files.isDirectory(root)) return ""
return runCatching {
Files.list(root).use { stream ->
stream
.filter { Files.isDirectory(it) }
.map { dir ->
val meta = dir.resolve(SavingContext.METADATA_EDN)
if (!Files.isRegularFile(meta)) return@map ""
val name = dir.fileName?.toString().orEmpty()
val mtime = runCatching { Files.getLastModifiedTime(meta).toMillis() }.getOrDefault(0L)
val size = runCatching { Files.size(meta) }.getOrDefault(0L)
"$name:$mtime:$size"
}
.filter { it.isNotBlank() }
.sorted()
.toList()
.joinToString("\n")
}
}.getOrDefault("")
}
private fun Any?.isTrueLike(): Boolean = when (this) {
is Boolean -> this
is Number -> this.toInt() != 0
is String -> this.equals("true", ignoreCase = true) || this == "1"
else -> false
}
private fun sourceCapsules(root: Path, index: KotlinSourceIndexDb?): List<SemanticCapsule> {
val indexedDefinitions = index?.definitions.orEmpty()
val definitions = if (indexedDefinitions.isEmpty()) {
scanDefinitions(root)
} else {
(indexedDefinitions + scanDefinitions(root, listOf("src/generatedLinalg/kotlin")))
.distinctBy { Triple(it.filePath, it.line, it.name) }
}
val linesCache = HashMap<String, List<String>>()
fun lines(path: String): List<String> {
return linesCache.getOrPut(path) {
val file = root.resolve(path).normalize()
runCatching { Files.readAllLines(file, StandardCharsets.UTF_8) }.getOrDefault(emptyList())
}
}
return definitions.mapNotNull { def ->
val fileLines = lines(def.filePath)
val start = (def.line - 1).coerceAtLeast(0)
if (start >= fileLines.size) return@mapNotNull null
val source = declarationSource(fileLines, start)
val signature = source.lineSequence().firstOrNull { it.trim().isNotBlank() }?.trim().orEmpty()
val calls = extractCallNames(source).take(32)
val parentId = "container:${def.filePath}"
val metadata = buildString {
appendLine("file_hash: ${def.contentHash}")
appendLine("package: ${packageName(fileLines)}")
if (!def.completionCategory.isNullOrBlank()) appendLine("completion_category: ${def.completionCategory}")
if (calls.isNotEmpty()) {
appendLine("calls:")
calls.forEach { appendLine("- $it") }
}
}.trim()
SemanticCapsule(
id = "decl:${def.filePath}:${def.line}:${def.kind}:${def.name}".sha256Id(),
scale = "declaration",
corpus = "field-internals",
kind = def.kind,
symbol = def.name,
title = def.name.substringAfterLast('.'),
path = def.filePath,
line = def.line,
endLine = start + source.lines().size,
parentId = parentId,
signature = signature,
documentation = def.kdoc.orEmpty().ifBlank { precedingKDoc(fileLines, start) },
metadata = metadata,
source = source
)
} + containerCapsules(root, definitions)
}
private fun containerCapsules(root: Path, definitions: List<KotlinDefinitionRecord>): List<SemanticCapsule> {
return definitions.groupBy { it.filePath }.map { (path, defs) ->
val file = root.resolve(path).normalize()
val packageName = runCatching { packageName(Files.readAllLines(file, StandardCharsets.UTF_8)) }.getOrDefault("")
SemanticCapsule(
id = "container:$path",
scale = "container",
corpus = "field-internals",
kind = "file",
symbol = path,
title = path.substringAfterLast('/'),
path = path,
line = 1,
metadata = buildString {
appendLine("package: $packageName")
appendLine("declarations:")
defs.take(80).forEach { appendLine("- ${it.kind} ${it.name}:${it.line}") }
}.trim()
)
}
}
private fun docsCapsules(root: Path): List<SemanticCapsule> {
val docsRoot = root.resolve("public/docs")
if (!Files.exists(docsRoot)) return emptyList()
val out = mutableListOf<SemanticCapsule>()
Files.walk(docsRoot).use { stream ->
stream
.filter { Files.isRegularFile(it) && it.toString().endsWith(".md") }
.forEach { file ->
val rel = root.relativize(file).toString().replace("\\", "/")
val lines = runCatching { Files.readAllLines(file, StandardCharsets.UTF_8) }.getOrDefault(emptyList())
// A redirect stub (`docs:` front matter, written beside each tutorial for its Guides panel) is a
// pointer to a page that is indexed itself. Indexed, the stubs took first place for the very
// sentences they quote: 196 of 231 misses in the first measured eval (2026-10-22).
if (MarkdownChunker.isRedirectStub(lines)) return@forEach
// A tutorial's files are indexed from its boxes in the `tutorials` corpus; its markdown mirror
// (document.md, AGENTS.md) would be a second copy of the same code in the documentation corpus.
if (rel.startsWith("public/docs/assets/agent_transcripts/") || rel.startsWith("public/docs/assets/canonical_graphics/") && !rel.endsWith("LESSONS.md")) return@forEach
// See MarkdownChunker for what was wrong with cutting at every `#` line.
val chunks = if (evalSetting("chunker") == "sections") markdownSections(lines).map {
MarkdownChunker.Chunk(it.title, it.title, it.level, it.line, it.endLine, it.body.take(10_000))
} else MarkdownChunker.chunks(lines)
chunks.forEachIndexed { idx, chunk ->
val title = chunk.title.ifBlank { rel.substringAfterLast('/') }
out += SemanticCapsule(
id = "docs:$rel:${chunk.line}:$idx".sha256Id(),
scale = "container",
corpus = "documentation",
// The generated API reference is half the corpus; its own kind lets a search ask for
// it or leave it out. Measured, leaving it out by default was a net loss.
kind = if ("/reference/" in rel) "docs-reference" else "docs-section",
symbol = chunk.breadcrumb.ifBlank { title },
title = if (chunk.part > 0) "$title (part ${chunk.part})" else title,
path = rel,
line = chunk.line,
endLine = chunk.endLine,
parentId = "subsystem:docs:${rel.substringBeforeLast('/', "")}",
documentation = chunk.body
)
}
}
}
return dedupeDocs(out)
}
Identical text indexed once: each tutorial ships the same AGENTS.md (256 copies), and a copy outranks
nothing but itself. The kept copy is the one outside assets/, then the shortest path.
source — 272 lines, from line 1946
private fun dedupeDocs(capsules: List<SemanticCapsule>): List<SemanticCapsule> =
capsules.groupBy { it.documentation.lowercase().replace(Regex("\\s+"), " ").trim().sha256Id() }
.values.map { copies -> copies.minWith(compareBy<SemanticCapsule>({ "/assets/" in it.path }, { it.path.length }, { it.path })) }
private fun subsystemCapsules(capsules: List<SemanticCapsule>): List<SemanticCapsule> {
val bySubsystem = capsules
.filter { it.path.isNotBlank() && it.path != "." }
.groupBy { capsule ->
when {
capsule.path.startsWith("public/docs/") -> "docs:${capsule.path.removePrefix("public/docs/").substringBefore('/')}"
capsule.path.startsWith("src/") -> capsule.path.split('/').take(5).joinToString("/")
else -> capsule.path.substringBefore('/')
}
}
return bySubsystem.map { (subsystem, rows) ->
SemanticCapsule(
id = "subsystem:$subsystem",
scale = "subsystem",
corpus = rows.groupingBy { it.corpus }.eachCount().maxByOrNull { it.value }?.key ?: "mixed",
kind = "subsystem",
symbol = subsystem,
title = subsystem,
path = rows.minByOrNull { it.path }?.path.orEmpty(),
line = 1,
metadata = buildString {
appendLine("items: ${rows.size}")
rows.take(100).forEach { appendLine("- ${it.kind} ${it.symbol} ${it.path}:${it.line}") }
}.trim()
)
}
}
private fun precedingKDoc(lines: List<String>, declarationIndex: Int): String {
var cursor = declarationIndex - 1
while (cursor >= 0 && lines[cursor].isBlank()) cursor--
if (cursor < 0 || !lines[cursor].trim().endsWith("*/")) return ""
val reversed = mutableListOf<String>()
while (cursor >= 0) {
val line = lines[cursor]
reversed += line
if (line.trim().startsWith("/**")) break
cursor--
}
if (reversed.lastOrNull()?.trim()?.startsWith("/**") != true) return ""
return reversed.asReversed()
.joinToString("\n")
.removePrefix("/**")
.removeSuffix("*/")
.lineSequence()
.map { it.trim().removePrefix("*").trim() }
.joinToString("\n")
.trim()
}
private fun scanDefinitions(
root: Path,
roots: List<String> = listOf(
"src/jvmMain/kotlin",
"src/commonMain/kotlin",
"src/personal/kotlin",
"src/generatedLinalg/kotlin",
),
): List<KotlinDefinitionRecord> {
val out = mutableListOf<KotlinDefinitionRecord>()
val sourceRoots = roots
.map { root.resolve(it) }
.filter { Files.exists(it) }
sourceRoots.forEach { sourceRoot ->
Files.walk(sourceRoot).use { stream ->
stream.filter { Files.isRegularFile(it) && it.toString().endsWith(".kt") }.forEach { file ->
val rel = root.relativize(file).toString().replace("\\", "/")
val text = runCatching { Files.readString(file, StandardCharsets.UTF_8) }.getOrDefault("")
val hash = text.sha256Id()
Regex("""(?m)^(\s*)(?:[\w\s@().<>"]+\s+)?(fun|val|var|class|interface|object)\s+([A-Za-z_][A-Za-z0-9_]*)""")
.findAll(text)
.forEach { match ->
val line = text.substring(0, match.range.first).count { it == '\n' } + 1
out += KotlinDefinitionRecord(
name = match.groupValues[3],
kind = match.groupValues[2],
filePath = rel,
line = line,
fileId = null,
contentHash = hash
)
}
}
}
}
return out
}
private fun SemanticCapsule.toLuceneDocument(intentVector: FloatArray?, implementationVector: FloatArray?): Document =
Document().also { doc ->
doc.add(StringField("id", id, Field.Store.YES))
doc.add(StringField("scale", scale, Field.Store.YES))
doc.add(StringField("corpus", corpus, Field.Store.YES))
doc.add(StringField("kind", kind, Field.Store.YES))
doc.add(StringField("path", path, Field.Store.YES))
doc.add(StringField("parentId", parentId, Field.Store.YES))
doc.add(StringField("documentName", documentName, Field.Store.YES))
doc.add(StringField("boxId", boxId, Field.Store.YES))
doc.add(StringField("property", property, Field.Store.YES))
doc.add(StringField("language", language, Field.Store.YES))
doc.add(StoredField("boxName", boxName))
doc.add(StoredField("symbol", symbol))
doc.add(StoredField("title", title))
doc.add(TextField("symbolText", splitIdentifier(symbol), Field.Store.NO))
doc.add(TextField("titleText", splitIdentifier(title), Field.Store.NO))
doc.add(TextField("boxNameText", splitIdentifier(boxName), Field.Store.NO))
doc.add(TextField("signature", signature, Field.Store.YES))
doc.add(TextField("documentation", documentation, Field.Store.YES))
doc.add(TextField("metadata", metadata, Field.Store.YES))
doc.add(TextField("source", source.take(30_000), Field.Store.YES))
doc.add(TextField("all", lexicalText().take(80_000), Field.Store.NO))
doc.add(TextField("allStem", lexicalText().take(80_000), Field.Store.NO))
doc.add(StoredField("line", line))
doc.add(StoredField("endLine", endLine))
doc.add(StoredField("capsule", intentText().take(30_000)))
if (intentVector != null) doc.add(KnnFloatVectorField(INTENT_VECTOR, intentVector, VectorSimilarityFunction.COSINE))
if (implementationVector != null) doc.add(KnnFloatVectorField(IMPLEMENTATION_VECTOR, implementationVector, VectorSimilarityFunction.COSINE))
}
private fun buildBm25Query(raw: String): Query {
val exact = buildExactBm25Query(raw)
val boost = stemBoost()
if (boost <= 0f) return exact
val stemmed = runCatching {
org.apache.lucene.queryparser.classic.QueryParser("allStem", stemAnalyzer).parse(MultiFieldQueryParser.escape(raw))
}.getOrNull() ?: return exact
return BooleanQuery.Builder()
.add(exact, BooleanClause.Occur.SHOULD)
.add(org.apache.lucene.search.BoostQuery(stemmed, boost), BooleanClause.Occur.SHOULD)
.build()
}
private fun buildExactBm25Query(raw: String): Query {
val parser = MultiFieldQueryParser(
arrayOf("all", "documentation", "metadata", "source", "signature", "title"),
analyzer,
mapOf("title" to 2.5f, "signature" to 2.0f, "documentation" to 1.6f, "metadata" to 1.3f)
)
return runCatching { parser.parse(MultiFieldQueryParser.escape(raw)) }
.getOrElse { TermQuery(Term("all", raw.lowercase(Locale.ROOT))) }
}
private fun buildSymbolQuery(raw: String): Query {
val builder = BooleanQuery.Builder()
// Through the index's own analyzer, so a stemmed or split index term is met by the same term.
analyzedTokens(splitIdentifier(raw)).forEach { token ->
builder.add(TermQuery(Term("symbolText", token)), BooleanClause.Occur.SHOULD)
builder.add(TermQuery(Term("titleText", token)), BooleanClause.Occur.SHOULD)
builder.add(TermQuery(Term("boxNameText", token)), BooleanClause.Occur.SHOULD)
if (token.length > 2) builder.add(TermQuery(Term("signature", token)), BooleanClause.Occur.SHOULD)
}
return builder.build().takeIf { it.clauses().isNotEmpty() } ?: buildBm25Query(raw)
}
private fun Array<ScoreDoc>.toRanked(
searcher: IndexSearcher,
filter: (Document) -> Boolean,
scoreScale: Float = 1f
): List<RankedDoc> =
mapIndexedNotNull { idx, sd ->
val doc = searcher.storedFields().document(sd.doc)
if (!filter(doc)) null else RankedDoc(sd.doc, idx + 1, sd.score * scoreScale)
}
private fun mergeVectorRanks(a: List<RankedDoc>, b: List<RankedDoc>): List<RankedDoc> {
val merged = LinkedHashMap<Int, RankedDoc>()
(a + b).forEach { row ->
val existing = merged[row.docId]
if (existing == null || row.score > existing.score) {
merged[row.docId] = row
}
}
return merged.values.sortedByDescending { it.score }.mapIndexed { idx, row -> row.copy(rank = idx + 1) }
}
private fun fuseRanks(
searcher: IndexSearcher,
vector: List<RankedDoc>,
bm25: List<RankedDoc>,
symbol: List<RankedDoc>,
limit: Int,
request: SemanticRetrievalQueryRequest
): List<SemanticRetrievalHit> {
val ids = LinkedHashSet<Int>()
ids += vector.map { it.docId }
ids += bm25.map { it.docId }
ids += symbol.map { it.docId }
val vectorByDoc = vector.associateBy { it.docId }
val bm25ByDoc = bm25.associateBy { it.docId }
val symbolByDoc = symbol.associateBy { it.docId }
return ids.map { docId ->
val vectorHit = vectorByDoc[docId]
val bm25Hit = bm25ByDoc[docId]
val symbolHit = symbolByDoc[docId]
val score = rrf(vectorHit?.rank) + rrf(bm25Hit?.rank) + 0.5 * rrf(symbolHit?.rank)
val doc = searcher.storedFields().document(docId)
hitFromDoc(
doc = doc,
score = score,
vector = vectorHit,
bm25 = bm25Hit,
symbol = symbolHit,
includeCapsule = request.includeCapsule,
includeSource = request.includeSource
)
}.sortedByDescending { it.score }
.take(limit)
}
private fun hitFromDoc(
doc: Document,
score: Double,
vector: RankedDoc?,
bm25: RankedDoc?,
symbol: RankedDoc?,
includeCapsule: Boolean,
includeSource: Boolean
): SemanticRetrievalHit =
SemanticRetrievalHit(
id = doc.get("id").orEmpty(),
scale = doc.get("scale").orEmpty(),
corpus = displayCorpusName(doc.get("corpus").orEmpty()),
kind = doc.get("kind").orEmpty(),
symbol = doc.get("symbol").orEmpty(),
title = doc.get("title").orEmpty(),
path = doc.get("path").orEmpty(),
line = doc.getField("line")?.numericValue()?.toInt() ?: 0,
endLine = doc.getField("endLine")?.numericValue()?.toInt() ?: 0,
parentId = doc.get("parentId").orEmpty(),
documentName = doc.get("documentName").orEmpty(),
boxName = doc.get("boxName").orEmpty(),
boxId = doc.get("boxId").orEmpty(),
property = doc.get("property").orEmpty(),
language = doc.get("language").orEmpty(),
// Coerce non-finite scores to 0: a degenerate/zero embedding vector yields a
// NaN cosine score, which kotlinx serializes as the bare token `NaN` — invalid
// JSON that browsers' response.json() rejects, silently breaking every
// in-browser semantic search (palette, welcome, source-card links). Python's
// json accepts NaN, which is why curl masked it.
score = if (score.isFinite()) score else 0.0,
vectorRank = vector?.rank ?: 0,
bm25Rank = bm25?.rank ?: 0,
symbolRank = symbol?.rank ?: 0,
vectorScore = (vector?.score ?: 0f).let { if (it.isFinite()) it else 0f },
bm25Score = (bm25?.score ?: 0f).let { if (it.isFinite()) it else 0f },
symbolScore = (symbol?.score ?: 0f).let { if (it.isFinite()) it else 0f },
capsule = if (includeCapsule) doc.get("capsule").orEmpty() else "",
sourcePreview = if (includeSource) doc.get("source").orEmpty().take(2000) else doc.get("signature").orEmpty()
)
private fun rrf(rank: Int?, k: Int = 60): Double = if (rank == null || rank <= 0) 0.0 else 1.0 / (k + rank)
private fun SemanticRetrievalQueryRequest.normalizedCorpora(): Set<String> =
(corpora + corpus.split(',', ';', '|'))
.map { normalizeCorpusName(it) }
.filter { it.isNotBlank() }
.filter { it != "all" }
.toSet()
private fun normalizeCorpusName(raw: String): String =
when (raw.trim().lowercase(Locale.ROOT)) {
"", "*", "all", "any" -> "all"
"field", "fields", "field4", "internals", "source", "src", "code", "field-internal", "field-internals" -> "field-internals"
"workspace", "workspace-sidecar", "workspace-sidecars", "sidecar", "sidecars", "document", "documents", "current-document" -> "workspace"
"doc", "docs", "documentation", "manual", "manuals" -> "documentation"
"tutorial", "tutorials", "tutorial-code", "examples", "starting-points", "starting-point" -> "tutorials"
else -> raw.trim().lowercase(Locale.ROOT)
}
The stored corpus terms for corpora (normalized names), or null for every corpus.
source — 93 lines, from line 2220
private fun corpusFilterQuery(corpora: Set<String>): Query? {
if (corpora.isEmpty() || "all" in corpora) return null
val stored = corpora.flatMap { if (it == "workspace") listOf("workspace", "workspace-sidecars") else listOf(it) }
return TermInSetQuery("corpus", stored.map(::BytesRef))
}
private fun displayCorpusName(raw: String): String =
raw.replace("workspace-sidecars", "workspace")
private data class RankedDoc(val docId: Int, val rank: Int, val score: Float)
private data class MarkdownSection(val title: String, val level: Int, val line: Int, val endLine: Int, val body: String)
private fun markdownSections(lines: List<String>): List<MarkdownSection> {
if (lines.isEmpty()) return emptyList()
val headings = lines.mapIndexedNotNull { idx, line ->
val match = Regex("""^(#{1,4})\s+(.+?)\s*$""").matchEntire(line) ?: return@mapIndexedNotNull null
Triple(idx, match.groupValues[1].length, match.groupValues[2].trim())
}
if (headings.isEmpty()) {
return listOf(MarkdownSection("", 1, 1, lines.size, lines.joinToString("\n").take(12_000)))
}
return headings.mapIndexed { index, heading ->
val start = heading.first
val endExclusive = headings.getOrNull(index + 1)?.first ?: lines.size
MarkdownSection(
title = heading.third,
level = heading.second,
line = start + 1,
endLine = endExclusive,
body = lines.subList(start, endExclusive).joinToString("\n").take(12_000)
)
}
}
private fun declarationSource(lines: List<String>, start: Int, maxLines: Int = 220): String {
var depth = 0
var seenBrace = false
val out = mutableListOf<String>()
for (idx in start until (start + maxLines).coerceAtMost(lines.size)) {
val line = lines[idx]
out += line
line.forEach { ch ->
if (ch == '{') {
depth++
seenBrace = true
} else if (ch == '}') {
depth--
}
}
if (seenBrace && depth <= 0) break
if (!seenBrace && idx > start && line.isBlank()) break
}
return out.joinToString("\n").trimEnd()
}
private fun packageName(lines: List<String>): String =
lines.firstNotNullOfOrNull { line ->
Regex("""^\s*package\s+([A-Za-z0-9_.]+)""").find(line)?.groupValues?.get(1)
}.orEmpty()
private fun extractCallNames(source: String): List<String> =
Regex("""\b([A-Za-z_][A-Za-z0-9_]*)\s*\(""")
.findAll(source)
.map { it.groupValues[1] }
.filterNot { it in kotlinKeywords }
.distinct()
.toList()
private fun splitIdentifier(raw: String): String {
val spaced = raw
.replace('.', ' ')
.replace('_', ' ')
.replace(Regex("""([a-z0-9])([A-Z])"""), "$1 $2")
return "$raw $spaced"
}
private fun analyzedTokens(text: String): List<String> {
val out = LinkedHashSet<String>()
analyzer.tokenStream("symbolText", text).use { stream ->
val term = stream.addAttribute(org.apache.lucene.analysis.tokenattributes.CharTermAttribute::class.java)
stream.reset()
while (stream.incrementToken()) term.toString().takeIf { it.length >= 2 }?.let(out::add)
stream.end()
}
return out.toList()
}
private val kotlinKeywords = setOf(
"if", "for", "while", "when", "return", "throw", "catch", "fun", "class", "object", "interface",
"val", "var", "try", "else", "do", "is", "as", "in", "super", "this"
)
}
class OnnxSemanticEmbedder private constructor
class OnnxSemanticEmbedder private constructor(
private val config: Config,
private val env: OrtEnvironment,
private val session: OrtSession,
private val tokenizer: HuggingFaceTokenizer,
private val actualProvider: String
) : SemanticEmbedder {
override val dimension: Int = config.dimension
override val backend: String
override val backend: String = buildString {
append("onnx-runtime-java:$actualProvider:${config.modelPath?.fileName ?: "model.onnx"}")
if (config.fixedTokenLength > 0) append(":fixed-${config.fixedBatchSize}x${config.fixedTokenLength}")
}
override fun encodeQueries(texts: List<String>): List<FloatArray>
override fun encodeQueries(texts: List<String>): List<FloatArray> =
encode(texts.map { config.queryPrefix + it })
override fun encodeDocuments(texts: List<String>): List<FloatArray>
body — 54 lines, from line 2330
override fun encodeDocuments(texts: List<String>): List<FloatArray> =
encode(texts.map { config.documentPrefix + it })
private fun encode(texts: List<String>): List<FloatArray> {
if (texts.isEmpty()) return emptyList()
val requestedSize = texts.size
val encodedTexts = if (config.fixedBatchSize > texts.size) {
texts + List(config.fixedBatchSize - texts.size) { "" }
} else {
texts
}
val encodings = tokenizer.batchEncode(encodedTexts.toTypedArray())
val maxLength = if (config.fixedTokenLength > 0) {
config.fixedTokenLength
} else {
encodings.maxOfOrNull { it.ids.size }?.coerceAtMost(config.maxTokens) ?: 0
}
if (maxLength <= 0) return texts.map { FloatArray(dimension) }
val ids = Array(encodings.size) { LongArray(maxLength) }
val mask = Array(encodings.size) { LongArray(maxLength) }
val typeIds = Array(encodings.size) { LongArray(maxLength) }
encodings.forEachIndexed { batch, encoding ->
copyPadded(encoding.ids, ids[batch], maxLength)
copyPadded(encoding.attentionMask, mask[batch], maxLength)
copyPadded(encoding.typeIdsOrEmpty(), typeIds[batch], maxLength)
}
val inputs = LinkedHashMap<String, OnnxTensor>()
fun addIfExpected(name: String, value: Array<LongArray>) {
if (session.inputNames.contains(name)) {
inputs[name] = OnnxTensor.createTensor(env, value)
}
}
addIfExpected("input_ids", ids)
addIfExpected("attention_mask", mask)
addIfExpected("token_type_ids", typeIds)
try {
session.run(inputs).use { result ->
// `result.get(0)` is the OnnxValue; its `.value` is the array. Until 2026-10-22 this read
// `result.firstOrNull()?.value`, which is the Map.Entry's value (the OnnxTensor itself), so
// pooling fell through to zeros: every query and every indexed vector since 2026-07-06 was
// zero, and the dense half of semantic search matched nothing.
val raw = result.get(0).value
val vectors = pooledVectors(raw, mask).map { it.normalized() }.take(requestedSize)
vectors.forEachIndexed { i, vector ->
check(texts[i].isBlank() || vector.any { it != 0f }) {
"Embedder produced a zero vector for non-empty text (output ${raw?.javaClass?.name})"
}
}
return vectors
}
} finally {
inputs.values.forEach { it.close() }
}
}
override fun close()
override fun close() {
runCatching { tokenizer.close() }
runCatching { session.close() }
}
private fun pooledVectors(raw: Any?, mask: Array<LongArray>): List<FloatArray> {
return when (raw) {
is Array<*> -> {
val first = raw.firstOrNull()
when (first) {
is FloatArray -> raw.filterIsInstance<FloatArray>().map { it.copyOf(dimension) }
is Array<*> -> poolTokenEmbeddings(raw, mask)
else -> error("Unexpected embedding model output element ${first?.javaClass?.name}")
}
}
is FloatArray -> listOf(raw.copyOf(dimension))
// Silent zeros here hid a broken embedder for three months; a wrong output type is a failure.
else -> error("Unexpected embedding model output ${raw?.javaClass?.name}")
}
}
@Suppress("UNCHECKED_CAST")
private fun poolTokenEmbeddings(raw: Array<*>, mask: Array<LongArray>): List<FloatArray> {
return raw.mapIndexed { batch, item ->
val tokens = item as? Array<FloatArray> ?: return@mapIndexed FloatArray(dimension)
val out = FloatArray(dimension)
var count = 0
tokens.forEachIndexed { tokenIndex, vector ->
if ((mask.getOrNull(batch)?.getOrNull(tokenIndex) ?: 0L) == 0L) return@forEachIndexed
val upto = minOf(dimension, vector.size)
for (i in 0 until upto) out[i] += vector[i]
count++
}
if (count > 0) for (i in out.indices) out[i] /= count.toFloat()
out
}
}
data class Config
data class Config(
val modelPath: Path?,
val tokenizerPath: Path?,
val dimension: Int,
val maxTokens: Int,
val queryPrefix: String,
val documentPrefix: String,
val provider: String,
val fixedTokenLength: Int,
val fixedBatchSize: Int
) {
fun createOrNull(): OnnxSemanticEmbedder?
body — 54 lines, from line 2434
fun createOrNull(): OnnxSemanticEmbedder? {
val model = modelPath?.takeIf { it.exists() } ?: return null
val tokenizerFile = tokenizerPath?.takeIf { it.exists() } ?: return null
return runCatching {
val env = OrtEnvironment.getEnvironment()
val (session, actualProvider) = createSession(env, model)
val tokenizer = HuggingFaceTokenizer.builder()
.optTokenizerPath(tokenizerFile)
.optTruncation(true)
.optPadding(true)
.optMaxLength(maxTokens)
.build()
OnnxSemanticEmbedder(this, env, session, tokenizer, actualProvider)
}.onFailure {
lprintln("warning,semantic-search", "failed to load ONNX embedder: ${it.message ?: it.javaClass.name}")
}.getOrNull()
}
private fun createSession(env: OrtEnvironment, model: Path): Pair<OrtSession, String> {
val options = OrtSession.SessionOptions()
options.setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT)
return when (provider.lowercase(Locale.ROOT)) {
"coreml", "coreml-mlprogram" -> {
runCatching {
if (provider.equals("coreml-mlprogram", ignoreCase = true)) {
options.addCoreML(EnumSet.of(CoreMLFlags.CREATE_MLPROGRAM))
} else {
options.addCoreML()
}
lprintln("info,semantic-search", "using ONNX Runtime CoreML execution provider")
env.createSession(model.absolutePathString(), options) to provider.lowercase(Locale.ROOT)
}.getOrElse { coreMlError ->
lprintln(
"warning,semantic-search",
"CoreML execution provider failed (${coreMlError.message ?: coreMlError.javaClass.simpleName}); falling back to CPU"
)
val fallback = OrtSession.SessionOptions()
fallback.setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT)
env.createSession(model.absolutePathString(), fallback) to "cpu-fallback"
}
}
"xnnpack" -> {
val actualProvider = runCatching {
options.addXnnpack(emptyMap())
lprintln("info,semantic-search", "using ONNX Runtime XNNPACK execution provider")
"xnnpack"
}.onFailure {
lprintln("warning,semantic-search", "XNNPACK execution provider unavailable: ${it.message ?: it.javaClass.simpleName}")
}.getOrDefault("cpu-fallback")
env.createSession(model.absolutePathString(), options) to actualProvider
}
else -> env.createSession(model.absolutePathString(), options) to "cpu"
}
}
companion object
fun fromSettings(settings: SemanticRetrievalConfig): Config
fun fromSettings(settings: SemanticRetrievalConfig): Config {
val provider = semanticSearchProviderSetting()
val maxTokens = settings.maxTokens.coerceIn(16, 8192)
val fixedShapes = semanticSearchFixedShapesSetting(provider)
return Config(
modelPath = settings.modelPath.trim().takeIf { it.isNotBlank() }?.let { Paths.get(it).toAbsolutePath().normalize() },
tokenizerPath = settings.tokenizerPath.trim().takeIf { it.isNotBlank() }?.let { Paths.get(it).toAbsolutePath().normalize() },
dimension = settings.dimension.coerceIn(1, 1024),
maxTokens = maxTokens,
queryPrefix = settings.queryPrefix,
documentPrefix = settings.documentPrefix,
provider = provider,
fixedTokenLength = if (fixedShapes) maxTokens else 0,
fixedBatchSize = if (fixedShapes) semanticSearchBatchSizeSetting(provider) else 0
)
}
fun disabledReason(config: Config): String
fun disabledReason(config: Config): String =
when {
config.modelPath == null -> "Set afield.semanticSearch.onnx.model / AFIELD_SEMANTIC_SEARCH_ONNX_MODEL to enable dense vectors."
!config.modelPath.exists() -> "Configured ONNX model does not exist: ${config.modelPath.absolutePathString()}"
config.tokenizerPath == null -> "Set afield.semanticSearch.onnx.tokenizer / AFIELD_SEMANTIC_SEARCH_ONNX_TOKENIZER to enable dense vectors."
!config.tokenizerPath.exists() -> "Configured tokenizer does not exist: ${config.tokenizerPath.absolutePathString()}"
else -> "ONNX embedder is configured but failed to load."
}
}
}
}
private fun copyPadded(source: LongArray, target: LongArray, maxLength: Int) {
val upto = minOf(source.size, maxLength)
for (i in 0 until upto) target[i] = source[i]
}
private fun Encoding.typeIdsOrEmpty(): LongArray =
runCatching { typeIds }.getOrDefault(LongArray(ids.size))
private fun FloatArray.normalized(): FloatArray {
var sum = 0.0
forEach { sum += (it * it).toDouble() }
val norm = sqrt(sum).toFloat()
if (norm <= 0f) return this
for (i in indices) this[i] /= norm
return this
}
private fun String.sha256Id(): String {
val digest = MessageDigest.getInstance("SHA-256").digest(toByteArray(StandardCharsets.UTF_8))
return digest.joinToString("") { "%02x".format(it) }
}