DEV Community

Cover image for Let’s build an AI app - pt. 2
Savannah Norem
Savannah Norem

Posted on • Updated on

Let’s build an AI app - pt. 2

As AI continues to make headlines, with Mistral releasing their first multi-modal model and OpenAI releasing a “reasoning” model, becoming an AI developer continues to be at the forefront of a lot of people's minds. And for most developers, knowing how to go from nothing to a functional website is the goal, so that’s what I’m here to attempt to teach you. If you’re already developing AI apps, I would love to hear about the tools you’re using, what you’re trying to do, and especially anything you’re struggling with.

If you want the quick and dirty of it - here you’ll find a demo and RedisVL data loader. RedisVL is the Redis Vector Library for Python, which is currently the go to language for AI development. In the last week I went from searching images of strawberries and returning the name of the closest images to searching images from anime posters and returning both the actual closest matched image and the name of the anime it came from. Below you'll see that a search for "swords" brings up the poster for Kimetsu no Yaiba: Katanakaji no Sato-hen, which prominently features...drumroll please...everyone holding a sword!

A search for the word

How'd we get here?

So last week I started looking for a dataset. If you’re getting into data science or machine learning - you’ve probably already heard of Kaggle and that’s where I started. I was thinking about some recommendations I’ve had for anime recently, and so started searching for anime datasets, I'm starting with this one but will have to do a bit of image gathering, since what's in the CSV is a link to the image, which is simply a limitation of CSVs.

So you have some data, now what?

The dataset I'm using has a lot more in it than I want, but filtering is a lot easier than supplementing and trying to tie two datasets together. There are 24 columns - anime_id, Name, English name, Other name, Score, Genres, Synopsis, Type, Episodes, Aired, Premiered, Status, Producers, Licensors, Studios, Source, Duration, Rating, Rank, Popularity, Favorites, Scored By, Members, Image URL

I decided I only wanted to look at TV shows and movies, so I filtered for that along with only storing the values that I care about (for now, always subject to change). The other thing about this dataset is that it has over 20,000 rows of anime in it. While that's totally great for them, I don't enjoy the time needed to fetch 20,000 images from their URLs. So I chopped mine to the top rated 1,000 - so I started with this:

sorted_anime = sorted(reader, key=lambda x: x[4])
anime_reader = sorted_anime[:1000]
Enter fullscreen mode Exit fullscreen mode

And if you're curious, in that particular dataset, the lowest rated is Aki no Puzzle at 2.37/10. Then I added a reverse=True to my sorting and found that UNKNOWN is a higher value than any number! Since I wanted this code to be follow-able and clean, I got rid of all of my print(row[5], type(row[5])) but just know that they were there! This is when I decided that the easiest way to handle some of the filtering I wanted to do would be to throw the whole CSV into a data frame, get rid of the stuff I don't want, then throw it back into a CSV to do some secondary parsing. If you go look at the code you may notice that I actually chop the CSV at 1,010 rows and have slightly more than 1,000 rows. That's because URLs can fail, and they might do so unpredictably. I figured a 1% error rate would be safe to make sure I have at least 1,000 in the end.

The problem with a CSV reader is that the only way to really use it is row by row. While that'll work just fine for changing the genres from a string to a list, it doesn't really work for sorting the CSV while excluding certain values. This should also save me a lot of time, since instead of having to check on each row for TV or Movie, and an actual numeric rating, I'll only be dealing with rows that have already cleared a few checks.

After getting the data how I want it, let’s make it searchable.
RedisVL uses a schema to define how your data looks, and for what I have currently, this is how my schema looks:

schema = {
    "index": {
        "name": "anime_demo",
        "prefix": "anime",
    },
    "fields": [
        {"name": "title", "type": "text"},
        {"name": "english_name", "type": "text"},
        {"name": "episodes", "type": "numeric"},
        {"name": "rating", "type": "numeric"},
        {"name": "synopsis", "type": "text"},
        {"name": "genres", "type": "tag"},
        {"name": "popularity_rank", "type": "numeric"},
        {"name": "poster_vector", "type": "vector", 
            "attrs": {
                 "dims": 768,
                 "distance_metric": "cosine",
                 "algorithm": "flat",
                 "datatype": "float32"
            }
         }
    ]
}
Enter fullscreen mode Exit fullscreen mode

We’ll have a title, an English name, the number of episodes, the numeric rating, a synopsis, the genres of the show or movie, the popularity ranking, and a vector embedding of the poster image. The dimensions of your vector will change based on what model you’re using - but knowing that information and correctly passing it along to Redis are critical steps to make vector search work correctly.

After that the changes to the Gradio portion were pretty simple. I changed up the structure a bit due to needing to pass the image in a format that would work for Gradio. So now we have a demo that contains the elements for the webpage and the search functionality, then we launch it.

with gr.Blocks() as demo:
   search_term = gr.Textbox(label="Search the top 100 anime by their 
      posters")
   search_results = gr.Textbox(label="Closest Anime (by poster 
      search)")
   poster = gr.Image(label="Closest Anime Poster")


   def anime_search(text):
       embedding = vectorizer.embed(text, as_buffer=True)
       query = VectorQuery(vector = embedding, vector_field_name = 
           "poster_vector", return_fields=["title", "img_name"])
       results = index.query(query)[0]
       title = results['title']
       img = results['img_name']
       return title, Image.open(f'anime_posters/{img}')

   gr.Button("Search").click(fn=anime_search, inputs=search_term, 
      outputs=[search_results, poster])


demo.launch()
Enter fullscreen mode Exit fullscreen mode

Things I'll do next

I'm still on the hunt for a bit more data about these anime - I would love a longer description and potentially more images. I definitely want to see how many I can add, and how many fields can be vectors, in a free tier Redis database.

I’m also planning to see what all Gradio can do - ideally I’d like to be able to cycle through the results and display both the poster and the anime title for about the top 5 results from the vector search.

The other thing I’m going to do this week is a bit of refactoring. I’d like to add some of the standard best practices to this repository, like a real README and a requirements.txt that will help you try it out if you want.

I would love to hear from you about the AI apps you’re building, if you know how to scroll through images in Gradio, and if you have favorite tools for scraping dynamic web content. Check out the RedisVL project yourself if you’re considering an AI app backed by Redis, as it really does make this whole thing so much easier.

Top comments (0)