Akan.js
Docs
문서컨벤션레퍼런스Cheatsheet
Akan.js
문서컨벤션레퍼런스Cheatsheet
Akan.js

MIT 라이선스 하에 배포되었습니다.

Akan.js 공식 컨설팅 서비스AkansoftCopyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman
사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
일반▾
인증과 권한에이전트를 위한 OAuth스키마 설계텍스트 검색엣지 컴퓨팅파일 관리Single Sign-OnDataList & Enum
인터페이스▾
CRUDEndpointMCP 서버에이전트 채팅Form
관측성▾
로깅의존성 주입에러 처리메트릭
성능▾
캐싱이미지 최적화지연 로딩쿼리변경큐실시간
모바일▾
설정Push NotificationsDeep LinksUI & Keyboard데스크톱 배포
개발▾
문서화스키마 문서스크립트콘솔도커쿠버네티스PWA테스트
사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
일반▾
인증과 권한에이전트를 위한 OAuth스키마 설계텍스트 검색엣지 컴퓨팅파일 관리Single Sign-OnDataList & Enum
인터페이스▾
CRUDEndpointMCP 서버에이전트 채팅Form
관측성▾
로깅의존성 주입에러 처리메트릭
성능▾
캐싱이미지 최적화지연 로딩쿼리변경큐실시간
모바일▾
설정Push NotificationsDeep LinksUI & Keyboard데스크톱 배포
개발▾
문서화스키마 문서스크립트콘솔도커쿠버네티스PWA테스트
이전스키마 설계다음엣지 컴퓨팅

텍스트 검색

Akan에는 전문 검색이 내장되어 있습니다. 따로 띄울 검색 서버도, 손으로 맞춰야 할 인덱스도 없습니다.
세 단계면 됩니다:
  1. field 표시하기. constant.ts에서 검색할 field마다 text 역할을 붙입니다.
  2. filter 작성하기. document.ts의 filter에서 q.search(text)를 호출합니다.
  3. 호출하기. 생성된 listBySearch를 "relevance" 정렬로 부릅니다.
클라이언트에서도 검색하게 할지는 별도의 결정입니다. 아래 '클라이언트에 공개하기'에서 다룹니다.
검색은 모든 데이터베이스 모드에서 동작합니다. 같은 텍스트라면 SQLite, libSQL, Postgres 모두 같은 document를 찾지만, Postgres는 단어가 얼마나 드문지를 순위에 반영하지 않아서 순서가 다를 수 있습니다. Postgres 설정은 아래 '운영하기'에서 다룹니다.
이 페이지에서 쓰는 말
용어설명
text
field를 검색 인덱스에 넣는 옵션입니다. { text: "title" }처럼 씁니다.
filter
document.ts에 이름을 붙여 선언한 query입니다. service에 그 이름을 딴 listBySearch 같은 메서드가 생깁니다.
q.search()
텍스트를 인덱스와 대조하는 query 노드입니다.
slice
filter를 클라이언트 store가 불러올 수 있는 endpoint로 공개한 것입니다.
relevance
가장 잘 맞는 결과를 먼저 놓는 기본 제공 정렬 키입니다.

1. Field 표시하기

field 옵션에 { text: "title" }처럼 역할을 적습니다. 얼마나 잘 검색되길 바라는지가 아니라 값이 무엇인지를 보고 고르세요. 역할마다 순위 가중치가 다릅니다.
다섯 역할을 모두 쓴 상품 모델입니다:
apps/shop/lib/product/product.constant.ts
역할가중치
↳ 용도
title10
사람이 검색창에 치는 이름입니다. 다른 어떤 역할보다 순위에 크게 반영됩니다.
tag3
키워드 목록입니다. 제목보다 낮고 본문보다 높습니다.
desc1
본문입니다. 매치는 되지만 이름이 맞은 결과보다 앞서지 않도록 가중치가 낮습니다.
filter0
status나 owner처럼 범위를 좁히는 값입니다. 검색에는 걸리지만 순위는 올리지 않습니다.
thumb—
검색 결과를 화면에 그릴 때 쓰도록 함께 저장됩니다. 색인되지 않으므로 매치되지 않습니다.
  • title, tag, desc는 String을 받습니다. filter와 thumb은 ID나 field(File) 같은 관계 field도 받고, 문자열 enum은 String으로 취급됩니다.
  • 배열과 내장 스칼라에도 쓸 수 있습니다. [String]은 항목마다 색인되고, 내장 스칼라 안의 역할은 부모 field를 통해 색인됩니다. Map 안의 field는 색인되지 않습니다.
  • 역할만 선언하면 끝입니다. 모델 단위로 켜는 스위치는 따로 없고, 인덱스는 선언한 역할을 그대로 따릅니다.

2. Filter 작성하기

q.search()는 다른 것과 똑같은 query 노드라서 q.all() 안에서 일반 조건과 조합됩니다. service에서 검색하는 데 slice는 필요 없습니다.
filter 선언하기
상태로도 좁힐 수 있는 검색 filter입니다:
apps/shop/lib/product/product.document.ts
  • .arg()는 필수 인자이고, .opt()는 생략할 수 있으며 모든 .arg() 뒤에 둡니다. 둘 다 선언한 순서대로 .query()에 들어오고, 마지막에 q가 붙습니다.
  • 빈 {}는 아무 조건도 더하지 않습니다. 상태를 넘기지 않으면 검색어만으로 결과를 좁힙니다.
다른 filter처럼 dictionary의 .query()에 인자까지 이름을 적어 줍니다:
apps/shop/lib/product/product.dictionary.ts
service에서 호출하기
filter를 선언하면 service에 메서드 묶음이 생깁니다. 검색에 쓰는 것은 이 네 개입니다:
메서드설명
listBySearch
매치된 document 목록입니다. 마지막 인자 { sort, skip, limit }로 정렬하고 페이지를 나눕니다.
countBySearch
매치되는 document 수입니다.
insightBySearch
매치된 document만으로 계산한 모델의 insight입니다.
queryBySearch
query 자체입니다. slice의 exec이 반환할 때 씁니다.
결과 한 페이지와 전체 개수를 돌려주는 service 메서드입니다:
apps/shop/lib/product/product.service.ts

3. 매칭 다듬기

옵션 세 가지면 거의 다 됩니다. 모두 q.search()의 두 번째 인자로 넘깁니다.
prefixboolean기본값 false
마지막 단어를 접두어로도 매치합니다. 입력하면서 찾는 검색창용이며, 없으면 Ken으로 Kenny를 못 찾습니다.
columns("title" | "desc" | "tag" | "filter")[]기본값 네 개 모두
지정한 역할에서만 찾습니다. thumb은 색인되지 않으므로 고를 수 없습니다.
weights[title, desc, tag, filter]기본값 [10, 1, 3, 0]
순위 가중치를 바꿉니다. title, desc, tag, filter 순서대로 음수가 아닌 유한한 숫자 네 개를 넘깁니다.
입력은 이렇게 매치됩니다
  • 사용자 입력을 그대로 넣어도 안전합니다. 입력의 어떤 글자도 검색 문법으로 해석되지 않습니다. 문장부호는 단어를 조각으로 나누고 그 조각들이 나란히 있어야 매치되므로, follow-up은 “follow-up”과 “follow up”을 찾습니다.
  • 모든 단어가 매치되어야 합니다. 순서는 상관없고, 기본 토크나이저는 대소문자와 악센트를 구분하지 않습니다.
  • 빈 입력은 아무것도 매치하지 않습니다. 빈 검색창이 전체 목록이 되는 일은 없습니다.
정렬
sort 값
↳ 결과 순서
"relevance"
가장 잘 맞는 결과부터 옵니다.
다른 키 ("latest" 등)
점수보다 그 키가 우선합니다.
생략 — service에서 호출
query에 검색이 있으므로 가장 잘 맞는 결과부터 옵니다.
생략 — slice endpoint
"latest"가 채워지므로 점수 순이 되지 않습니다.

클라이언트에 공개하기

filter는 서버에서만 실행됩니다. slice를 달면 클라이언트가 부를 수 있는 endpoint가 되고, 공개 조회가 가능한 모델이라면 누구나 query를 반복하며 테이블 전체를 훑을 수 있게 됩니다.
그래서 모델마다 판단합니다:
검색되라고 있는 데이터
상품 목록입니다. 검색 slice를 공개하는 것이 곧 목적입니다.
대개 검색을 열지 않는 데이터
사용자 목록입니다. 검색 filter를 서버에만 둡니다.
자기 guard를 단 공개 상품 검색 slice입니다:
apps/shop/lib/product/product.signal.ts
  • dictionary에도 slice 이름을 적습니다. .slice() 아래에 .desc()와 함께 씁니다. MCP 에이전트는 그 설명을 보고 도구를 고릅니다.
  • 불러올 때 정렬을 명시합니다. st.do.initProductBySearch(text, statuses, { sort: "relevance" })
  • 실시간 검색 slice는 다시 불러오는 방식입니다. 검색은 메모리에서 대조할 수 없으므로, 검색을 담은 .live() slice는 { fallback: "invalidate" }를 선언합니다.
이름 붙은 slice는 자기 init({ guards })로만 보호됩니다. slice()의 guard 맵은 root slice와 생성된 CRUD에만 적용됩니다. 자기 guard가 없으면 HTTP로 누구나 호출할 수 있고, MCP에는 공개되지 않습니다.

운영하기

인덱스는 데이터베이스 trigger로 스스로 최신 상태를 유지합니다. document hook이 실행되지 않는 대량 query 단위 update까지, 어떤 경로의 write든 반영됩니다.
AKAN_SEARCH_ENABLED1 | true | 0 | false기본값 비우면 켜짐
인덱스를 켜고 끕니다. 꺼도 색인된 데이터는 남고, 다시 켜면 모든 모델을 다시 맞춰 색인합니다.
AKAN_SEARCH_TOKENIZERstring기본값 unicode61 remove_diacritics 2
fts5 토크나이저를 고릅니다. Postgres는 아래 두 형식만 읽습니다.
  • 한 배포의 모든 프로세스에 같은 값을 주세요. 프로세스는 자기가 마운트하지 않은 모델의 trigger를 정리하지 못하므로, 값이 섞이면 낡은 trigger가 남습니다.
  • 검색을 끈 동안에는 q.search()가 에러를 던집니다. q.search()를 선언한 filter 자체는 문제없고, 실제로 실행된 query만 실패합니다.
  • 토크나이저 변경은 가볍습니다. 다음 부팅 때 모델 테이블을 다시 읽지 않고 인덱스가 보관한 텍스트 사본에서 인덱스를 다시 만들므로, 부담 없이 다시 조정할 수 있습니다.
  • 한꺼번에 재시작해도 재생성은 한 번입니다. 첫 프로세스가 다시 만들고 나머지는 기다립니다. SQLite에서는 busy timeout(기본 5초)까지만 기다리므로, 인덱스가 크면 재시작을 나눠서 하세요.
Postgres에서
Postgres에만 해당하는 것이 세 가지 있습니다:
  • 토크나이저에 맞는 확장이 필요합니다. unicode61에는 unaccent가(remove_diacritics 0이면 불필요), trigram에는 pg_trgm이 필요합니다. Akan이 쓰는 데이터베이스 role에 권한이 있으면 Akan이 만들고, 없으면 권한이 있는 role로 CREATE EXTENSION unaccent(또는 pg_trgm)를 실행합니다.
  • 데이터베이스는 UTF-8 LC_CTYPE로 만듭니다. en_US.UTF-8이나 C.UTF-8 같은 값입니다. 그렇지 않으면 SQLite와 달리 ASCII 문자에서만 대소문자를 구분하지 않습니다.
  • 아주 긴 텍스트는 앞부분만 색인됩니다. unicode61에서 Postgres는 document마다 title, tag, filter 텍스트의 앞 20,000자와 desc의 앞 200,000자만 색인합니다.

주의할 점

q.search()를 둘 수 없는 곳과, 헷갈리기 쉬운 동작들입니다:
  • q.search()는 AND 위치에만 둘 수 있습니다. q.any()나 q.not() 아래에 두면 query가 에러를 던집니다.
  • query 단위 write에는 쓸 수 없습니다. 모델의 updateOne / updateMany / removeOne / removeMany, 그리고 생성된 updateBySearch / removeBySearch 계열은 대량 write가 인덱스와 join할 수 없어서 이를 거절합니다. 에러 메시지에는 updateOneByQuery나 updateManyByQuery라는 이름으로 나옵니다.
  • schema.index()는 검색과 무관합니다. schema.index({ name: "text" })라고 써도 일반 조회 인덱스만 만듭니다.
  • 지운 document는 인덱스에서도 빠집니다. soft delete도 마찬가지이고, 복구하면 다시 들어옵니다.

이 페이지

텍스트 검색
1. Field 표시하기
2. Filter 작성하기
3. 매칭 다듬기
클라이언트에 공개하기
운영하기
주의할 점
field.secret, field.hidden, resolve()에는 text 역할을 줄 수 없습니다. 인덱스는 평문을 저장하므로, 색인된 비밀 값은 검색으로 새어 나갑니다. 타입 검사에서 거절됩니다.
클라이언트에서는 "relevance"를 반드시 명시하세요. slice endpoint는 sort를 비워 두면 "latest"를 채우므로 점수 순으로 정렬되지 않습니다.