Bonjour à tous,
Aujourd’hui, nous allons nous attaquer à la partie la plus complexe du projet K-Droid Seforim : la base de données !
Avant de plonger dans les aspects techniques, voici les objectifs que nous souhaitons atteindre avec la base de données :
Après réflexion, j’ai décidé que la meilleure approche consiste à créer une base de données parallèle à celle de Sefaria, en exploitant leur API. Voici comment cela fonctionnerait :
Stockage local des appels API :
Imitation du comportement de l’API :
Mises à jour efficaces :
Un inconvénient majeur se trouve lorsque l'on va devoir rechercher quelque chose dans cette base de données. Nous verrons comment régler ce problème dans la section suivante sur le moteur de recherche, mais effectivement, nous allons devoir établir notre propre moteur de recherche pour réduire au maximum ce problème.
La première étape consiste à obtenir la liste de tous les livres classés de façon hiérarchique. Pour cela, Sefaria fournit un exemple d'appel à l'API avec HttpClient
, mais comme précédemment mentionné, nous utiliserons Ktor
:
val client = OkHttpClient()
val request = Request.Builder()
.url("https://www.sefaria.org/api/index/")
.get()
.addHeader("accept", "application/json")
.build()
val response = client.newCall(request).execute()
Cette requête permet de récupérer les informations nécessaires sur tous les livres disponibles dans la base de données de Sefaria.
Cependant, cette requête retourne un JSON qui peut atteindre 50 000 lignes. Pour mieux comprendre la structure des données, examinons les 100 premières lignes du résultat. Voici un extrait typique :
[
{
"contents": [
{
"categories": ["Tanakh", "Torah"],
"order": 1,
"primary_category": "Tanakh",
"enShortDesc": "Creation, the beginning of mankind, and stories of the patriarchs and matriarchs.",
"heShortDesc": "בריאת העולם, תחילתה של האנושות וסיפורי האבות והאמהות.",
"heTitle": "בראשית",
"title": "Genesis"
},
{
"categories": ["Tanakh", "Torah"],
"order": 2,
"primary_category": "Tanakh",
"enShortDesc": "The Israelites’ enslavement in Egypt, miraculous redemption, the giving of the Torah, and building of the Mishkan (Tabernacle).",
"heShortDesc": "שעבוד בני ישראל במצרים, יציאת מצרים, מתן תורה ובניית המשכן.",
"heTitle": "שמות",
"title": "Exodus"
}
]
}
]
Ce JSON illustre comment les livres sont classés par catégorie, titre, et descriptions. À partir de ce JSON, on voit qu'il va être assez simple de créer un arbre hiérarchique des livres.
Pour mieux organiser et manipuler ces données, nous pouvons commencer par structurer notre modèle de données en Kotlin en utilisant la librairie kotlinx.serialization
:
@Serializable
data class TableOfContent(
@SerialName("contents") val contents: List<ContentItem> = emptyList(),
@SerialName("order") var order: Double? = null,
@SerialName("enComplete") val enComplete: Boolean? = null,
@SerialName("heComplete") val heComplete: Boolean? = null,
@SerialName("enDesc") val enDesc: String? = null,
@SerialName("heDesc") val heDesc: String? = null,
@SerialName("enShortDesc") val enShortDesc: String? = null,
@SerialName("heShortDesc") val heShortDesc: String? = null,
@SerialName("heCategory") val heCategory: String? = null,
@SerialName("category") val category: String? = null
)
@Serializable
data class ContentItem(
@SerialName("contents") val contents: List<ContentItem>? = null,
@SerialName("categories") val categories: List<String>? = null,
@SerialName("order") var order: Double? = null,
@SerialName("primary_category") val primaryCategory: String? = null,
@SerialName("enShortDesc") val enShortDesc: String? = null,
@SerialName("heShortDesc") val heShortDesc: String? = null,
@SerialName("corpus") val corpus: String? = null,
@SerialName("heTitle") val heTitle: String? = null,
@SerialName("title") val title: String? = null,
@SerialName("enComplete") val enComplete: Boolean? = null,
@SerialName("heComplete") val heComplete: Boolean? = null,
@SerialName("enDesc") val enDesc: String? = null,
@SerialName("heDesc") val heDesc: String? = null,
@SerialName("heCategory") val heCategory: String? = null,
@SerialName("category") val category: String? = null
)
Cette structure reflète le JSON retourné par l'API de Sefaria, ce qui facilite la gestion des données hiérarchiques.
Nous pouvons déjà les afficher sous forme d’arbre hiérarchique en utilisant Jewel :
fun convertToTree(data: List<TableOfContent>): Tree<String> {
return buildTree {
data.forEach { toc ->
addNode(toc.heCategory ?: "Uncategorized") {
buildContentTree(this, toc.contents)
}
}
}
}
fun buildContentTree(treeBuilder: ChildrenGeneratorScope<String>, contents: List<ContentItem>?) {
contents?.forEach { content ->
val nodeName = content.heTitle ?: content.heCategory ?: "Untitled"
if (content.contents.isNullOrEmpty()) {
// Dernier enfant : utiliser addLeaf
treeBuilder.addLeaf(nodeName)
} else {
// Nœud intermédiaire : utiliser addNode
treeBuilder.addNode(nodeName) {
buildContentTree(this, content.contents)
}
}
}
}
suspend fun fetchTableOfContents(): List<TableOfContent> {
val client = HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
return client.get("https://www.sefaria.org/api/index/").body()
}
@OptIn(ExperimentalJewelApi::class)
@Composable
fun DisplayTree() {
var tree by remember { mutableStateOf<Tree<String>>(emptyTree()) }
LaunchedEffect(Unit) {
val data = fetchTableOfContents()
tree = convertToTree(data)
}
Box(
Modifier
.fillMaxSize()
.padding(16.dp)
) {
LazyTree(
tree = tree,
modifier = Modifier.fillMaxSize(),
onElementClick = { element ->
println("Clicked on: \${element.data}")
},
onElementDoubleClick = { element ->
println("Double clicked on: \${element.data}")
}
) { element ->
Box(Modifier.fillMaxWidth().padding(4.dp)) {
Text(element.data, Modifier.padding(2.dp))
}
}
}
}
Ce code permet de transformer les données structurées en un arbre affichable et interactif. Jewel offre une interface élégante et performante pour naviguer dans les données hiérarchiques. On remarquera toutefois que le chevron est orienté vers la gauche. Pour corriger ce comportement, il est nécessaire d’utiliser la méthode décrite dans cette discussion.
Une fois les informations récupérées, il sera crucial de supprimer toutes les données inutiles ou redondantes pour optimiser le stockage et l'accès futur.
Enfin, les données seront formatées pour s'adapter à la structure de la base de données locale et compressées pour minimiser leur poids tout en maintenant des performances optimales.