from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates import chess import chess.pgn import time from chess_search.sparse.bitmap import BitmapIndex from chess_search.sparse.bitmap.parser import parse from chess_search.sparse.bitmap.expressions import execute idx = BitmapIndex.load_from_hub("christopher/chess-index") games = {} pgn = open("games.pgn") i = 0 while (game := chess.pgn.read_game(pgn)): gid = f"{game.headers.get('White', '?')} vs {game.headers.get('Black', '?')} {game.headers.get('Date', '')}" board = game.board() boards = [] for move in game.mainline_moves(): board.push(move) boards.append(board.copy()) games[gid] = {"headers": dict(game.headers), "boards": boards} i += 1 app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") from pyroaring import BitMap from chess_search.sparse.bitmap.query import ( on_file, on_rank, in_center, on_kingside, on_queenside, empty_square, open_file, anywhere, ) @app.get("/") def read_root(request: Request): return templates.TemplateResponse(request=request, name="index.html") @app.post("/search") async def search(data: dict): start = time.time() board = chess.Board(data["fen"]) tokens = [f"{p.symbol()}_{chess.square_name(sq)}" for sq, p in board.piece_map().items()] matches = idx.query(tokens) resolved = idx.resolve(matches) seen_games = {} for game_id, move_idx in resolved: if game_id not in seen_games: seen_games[game_id] = move_idx results = [] for game_id, move_idx in seen_games.items(): g = games.get(game_id) if not g or move_idx >= len(g["boards"]): continue results.append({ "FEN": g["boards"][move_idx].fen(), "White": g["headers"].get("White", ""), "Black": g["headers"].get("Black", ""), "Event": g["headers"].get("Event", ""), "Date": g["headers"].get("Date", ""), "Result": g["headers"].get("Result", ""), "MatchedMove": move_idx, }) elapsed_ms = (time.time() - start) * 1000 return {"count": len(results), "results": results, "time_ms": elapsed_ms} @app.post("/text-search") async def text_search(data: dict): start = time.time() q = parse(data["query"]) matches = execute(q, idx) resolved = idx.resolve(matches) seen_games = {} for game_id, move_idx in resolved: if game_id not in seen_games: seen_games[game_id] = move_idx results = [] for game_id, move_idx in seen_games.items(): g = games.get(game_id) if not g or move_idx >= len(g["boards"]): continue results.append({ "FEN": g["boards"][move_idx].fen(), "White": g["headers"].get("White", ""), "Black": g["headers"].get("Black", ""), "Event": g["headers"].get("Event", ""), "Date": g["headers"].get("Date", ""), "Result": g["headers"].get("Result", ""), "MatchedMove": move_idx, }) elapsed_ms = (time.time() - start) * 1000 return {"count": len(results), "results": results, "time_ms": elapsed_ms}