use yo_common::Result;
use yo_common::num::parse_i64;
use yo_search::Registry;
use yo_search::field::{Field, Kind};
use yo_search::index::Index;
use yo_search::posts::Id;
use yo_search::sorted::Sorted;
use super::{Args, twelve};
use crate::dispatch::args;
use crate::reply::Out;
const NAME: &str = "_FT.DEBUG";
const NO_CTX: &[u8] = b"Can not create a search ctx";
const NO_INVIDX: &[u8] = b"Can not find the inverted index";
const NO_FIELD: &[u8] = b"Could not find given field in index spec";
const BAD_ID: &[u8] = b"bad id given";
const GONE: &[u8] = b"document was removed";
const NO_DOC: &[u8] = b"Document not found in index";
const BAD_MODE: &[u8] = b"Invalid argument. Expected REVEAL or OBFUSCATE as the last argument";
const NAMES: &[&str] = &[
"DUMP_INVIDX",
"DUMP_NUMIDX",
"DUMP_TAGIDX",
"IDTODOCID",
"DOCIDTOID",
"DOCINFO",
"DUMP_TERMS",
"GET_MAX_DOC_ID",
];
pub(super) fn run(reg: &mut Registry, args: Args<'_>, out: &mut Out) -> Result<()> {
let sub = args.get(1);
let wanted = |what: &str| args::wrong_arity_sub(NAME, what);
match sub {
_ if args::is(sub, b"HELP") => help(out),
_ if args::is(sub, b"DUMP_TERMS") => {
if args.len() != 3 {
return Err(wanted("DUMP_TERMS"));
}
if let Some(index) = open(reg, args.get(2), out) {
terms(index, out);
}
}
_ if args::is(sub, b"DUMP_INVIDX") => {
if args.len() != 4 {
return Err(wanted("DUMP_INVIDX"));
}
if let Some(index) = open(reg, args.get(2), out) {
postings(index, args.get(3), out);
}
}
_ if args::is(sub, b"DUMP_TAGIDX") => {
if args.len() != 4 {
return Err(wanted("DUMP_TAGIDX"));
}
if let Some(index) = open(reg, args.get(2), out) {
tags(index, args.get(3), out);
}
}
_ if args::is(sub, b"DUMP_NUMIDX") => {
if args.len() != 4 {
return Err(wanted("DUMP_NUMIDX"));
}
if let Some(index) = open(reg, args.get(2), out) {
numbers(index, args.get(3), out);
}
}
_ if args::is(sub, b"DOCINFO") => {
if args.len() < 5 {
return Err(wanted("DOCINFO"));
}
if let Some(index) = open(reg, args.get(2), out) {
about(index, args.get(3), args.get(4), out);
}
}
_ if args::is(sub, b"IDTODOCID") => {
if args.len() != 4 {
return Err(wanted("IDTODOCID"));
}
if let Some(index) = open(reg, args.get(2), out) {
keyed(index, args.get(3), out);
}
}
_ if args::is(sub, b"DOCIDTOID") => {
if args.len() != 4 {
return Err(wanted("DOCIDTOID"));
}
if let Some(index) = open(reg, args.get(2), out) {
numbered(index, args.get(3), out);
}
}
_ if args::is(sub, b"GET_MAX_DOC_ID") => {
if args.len() != 3 {
return Err(wanted("GET_MAX_DOC_ID"));
}
if let Some(index) = open(reg, args.get(2), out) {
out.uint(u64::from(index.held.docs.last()));
}
}
_ => return Err(args::unknown_subcommand(sub, NAME)),
}
Ok(())
}
fn open<'a>(reg: &'a mut Registry, name: &[u8], out: &mut Out) -> Option<&'a Index> {
if reg.get(name).is_none() {
out.error(NO_CTX);
return None;
}
reg.open(name).map(|index| &*index)
}
fn help(out: &mut Out) {
out.array(NAMES.len());
for name in NAMES {
out.bulk(name.as_bytes());
}
}
fn terms(index: &Index, out: &mut Out) {
out.array(index.held.words());
for term in index.held.terms() {
out.bulk(term);
}
}
fn postings(index: &Index, term: &[u8], out: &mut Out) {
let Some(posts) = index.held.posts(term) else {
out.error(NO_INVIDX);
return;
};
out.array(posts.len() as usize);
let mut reader = posts.read();
while let Some(post) = reader.step() {
out.uint(u64::from(post.id));
}
}
fn tags(index: &Index, attribute: &[u8], out: &mut Out) {
if !kind(index, attribute, |kind| matches!(kind, Kind::Tag(_))) {
out.error(NO_FIELD);
return;
}
let Some(held) = index.held.values(attribute) else {
out.array(0);
return;
};
out.array(held.len());
for (value, ids) in held.all() {
out.array(2);
out.bulk(value);
out.array(ids.len());
for id in ids {
out.uint(u64::from(*id));
}
}
}
fn numbers(index: &Index, attribute: &[u8], out: &mut Out) {
let numeric = kind(index, attribute, |kind| matches!(kind, Kind::Numeric));
let geo = kind(index, attribute, |kind| matches!(kind, Kind::Geo));
if !numeric && !geo {
out.error(NO_FIELD);
return;
}
let ids = match numeric {
true => index.held.numbers(attribute).map(|held| held.ids()),
false => index.held.places(attribute).map(|held| held.ids()),
};
let Some(ids) = ids.filter(|ids| !ids.is_empty()) else {
out.array(0);
return;
};
out.array(1);
out.array(ids.len());
for id in ids {
out.uint(u64::from(id));
}
}
fn about(index: &Index, key: &[u8], mode: &[u8], out: &mut Out) {
let Some(id) = index.held.docs.id(key) else {
out.error(NO_DOC);
return;
};
let hide = if args::is(mode, b"OBFUSCATE") {
true
} else if args::is(mode, b"REVEAL") {
false
} else {
out.error(BAD_MODE);
return;
};
let Some(doc) = index.held.docs.get(id) else {
out.error(NO_DOC);
return;
};
let sortable: Vec<(usize, &Field, Option<&Sorted>)> = index
.schema
.iter()
.enumerate()
.filter(|(_, field)| field.sortable)
.enumerate()
.map(|(slot, (at, field))| (at, field, doc.sorted(slot)))
.collect();
let carried = sortable.iter().any(|(_, _, value)| value.is_some());
let pairs = 6 + usize::from(carried);
if out.proto().is_resp3() {
out.map(pairs);
} else {
out.array(pairs * 2);
}
out.simple(b"internal_id");
out.uint(u64::from(id));
out.bulk(b"flags");
out.bulk(&flags(index, doc.payload.is_some(), carried));
out.simple(b"score");
out.double(doc.score);
out.simple(b"num_tokens");
out.uint(u64::from(doc.tokens));
out.simple(b"max_freq");
out.uint(u64::from(doc.top));
out.simple(b"refcount");
out.uint(1);
if !carried {
return;
}
out.simple(b"sortables");
out.array(sortable.len());
for (slot, (at, field, value)) in sortable.iter().enumerate() {
out.array(6);
out.simple(b"index");
out.uint(slot as u64);
out.bulk(b"field");
out.bulk(&named(field, *at, hide));
out.bulk(b"value");
match value {
Some(Sorted::Number(number)) => out.bulk(twelve(*number).as_bytes()),
Some(Sorted::Text(text)) => out.bulk(text),
None => out.nil(),
}
}
}
fn flags(index: &Index, payload: bool, sortable: bool) -> Vec<u8> {
let offsets = !index.definition.options.nooffsets;
let bits = (u8::from(payload) << 1) | (u8::from(sortable) << 2) | (u8::from(offsets) << 3);
let mut out = format!("(0x{bits:x}):").into_bytes();
for (held, name) in [
(payload, "HasPayload,"),
(sortable, "HasSortVector,"),
(offsets, "HasOffsetVector,"),
] {
if held {
out.extend_from_slice(name.as_bytes());
}
}
out
}
fn named(field: &Field, at: usize, hide: bool) -> Vec<u8> {
let mut out = Vec::new();
if hide {
out.extend_from_slice(format!("FieldPath@{at} AS Field@{at}").as_bytes());
return out;
}
out.extend_from_slice(&field.identifier);
out.extend_from_slice(b" AS ");
out.extend_from_slice(&field.attribute);
out
}
fn keyed(index: &Index, arg: &[u8], out: &mut Out) {
let Some(number) = parse_i64(arg) else {
out.error(BAD_ID);
return;
};
let id = Id::try_from(number).ok();
match id.and_then(|id| index.held.docs.key(id)) {
Some(key) => out.bulk(key),
None => out.error(GONE),
}
}
fn numbered(index: &Index, key: &[u8], out: &mut Out) {
out.uint(u64::from(index.held.docs.id(key).unwrap_or(0)));
}
fn kind(index: &Index, attribute: &[u8], want: impl Fn(&Kind) -> bool) -> bool {
index
.field(attribute)
.is_some_and(|field| want(&field.kind))
}