Match with UMass folks without any commitments.
During the quarantine, many students (especially new students) have not been able to meet new people because everyone is remote. Our application aims to bridge that gap by helping students meet their UMass peers with similar interests. The main inspiration behind our app has been bringing our community together in these trying times.
U-Match is a first of its kind people-matching platform. Built to bring the community together and provide our users with a platform to meet and socialize with their UMass fellows, U-Match focuses on matching people based on commonalities.
Our users can request a match and, unlike most other matching-apps, U-Match will run an algorithm against our user-base to generate a match itself. The match is based on similarities such as majors, current/past UMass residences, clubs, interests in music, movie and video game genres and much more.
The only catch is that you can only request a match every 3 days! This is meant to give our users the time to reach out to their matches and take the time to get to know them. Since U-Match creates the matches based on our own algorithm, which also randomly chooses from the final list of filtered users, it opens up the possibilities for our users to meet new people. Our app also makes sure that our users only match with people they've never matched with before, creating greater potential for them to expand their social network.
The beauty of our project lies in its simplicity. U-Match only requires basic data like the user's major(/s), clubs, where they've lived on UMass and their interests, and uses these simple parameters to generate an intelligent match. There is a lot of value in being minimal and our app explores this philosophy down to its algorithm.
We separated the server and client and started working on our respective components by separating branches on git. For the server, I started by structuring the main components: models, schemas, api and crud into packages. Then, after I had a test resource working, I created a User resource which had its own Profile and Contact resource (separate models), hence lending me a relationship hierarchy.
Once I had the basic POST and GET routes working for the user, I focused my time on creating a function which would detect, in incoming data from schemas, all relationships and if present, add them to the primary object and add the primary object to the database, cutting down the total number of database commits from `2x` (for x relations) to 1. This was a game-changer since now I could simply define realtionships in schemas and models and not change the API route handler at all, and it would automatically resolve relationships.
With this, I containerized my application using Docker and docker-compose and also had a Postgres container attached so that I could test my app on Postgres as opposed to the default SQLite that ships with Python.
To create matches between users I, first, created a self-referential relationship for Users with Users but soon I realized how that would fail: infinite recursion. So I created another model for Match which stored ForeignKey references to the current user and the other (matched user). This seemed to solve my problem. At this point, I was testing this relationship and stuff like whether a user, who has matched in the past 3 days can request a match or not.
Then came the most important part of the project: the matching algorithm. First I dwelled into subqueries and complex SQLAlchemy magic but then I simplified my approach. My matching algorithm works in 4 parts:
1. For the given user, gets a list of all matchable users. A matchable user is someone who's not been matched with this user before (User's list of previous_matches: Match objects) and who's either not been matched ever before or in the past 3 days.
2. Using this list of matchable users and the current user's data, create a Filter object. Filter is a class implementing the FluentAPI pattern. It has methods to filter its own list of matchable users by comparing them to the current user by specific params. Each method returns the object itself (hence Fluent design).
3. A client-request for creating a match also sends, in the body, a list of booleans (profile fields) representing whether the user wants to match basis this field or not. If a boolean is true, its filter method is called on the Filter object created in (2). If any filter method results in an empty list, we disregard its result.
4. From the final filtered list, we return a random user.
With this, the server was complete and ready for deployment. I, first, considered using Terraform for provisioning our AWS resources but due to the time crunch, ended up resorting to the console. I made 2 security groups for the EC2 instance and RDS instance (both in staging) and made it so that the EC2 SG is accessible by everyone (and can be SSHed by me) and the RDS SG can only accessed by the EC2 SG. with that, we were deployed to staging and then I set up our AWS Cognito User Pool and then an AWS Lambda for presignup which autoconfirms our user. For this I had to set up a Cognito Identity Pool as well.
After setting up AWS Cognito, I set up an auth middleware which fetched the JWKS for our User Pool and uses its credentials to decode an incoming ID Token in the request header. This enables us to protect all routes.
For the React client, after setting up the basic wireframe, our main goal was connecting the app to the backend so that it can make requests to the server.
We faced a lot of challenges integrating our cloud services together and configuring the network topologies. The React app also fell into issues integrating with AWS Amplify.
We're proudest of a function in the server which automatically detects and resolves related data to an object and creates the object in the database, cutting down the total number of database commits from `2x` (for x relations) to 1.
We're am also really proud of our matching algorithm which uses FluentAPI pattern to match people based on what profile fields they prefer to match with others. This was a great refresher for our functional programming class.
Throughout building this project, we learned a multitude of things. We firstly learned how to work as a team on a time crunch, as we tried to polish up our project by the deadline. We also learned a lot about cloud deployment and server side programming for our backend, as well as frontend deployment for the frontend. It was overall a great experience to work on something like this.
Use UMass's OAuth system for user credentials; this can be used for making the API completely standalone. Add more fields to user profile. Optimize filtering. Use sphinx for Python documentation.
Backend: Python which depends on FastAPI (an async REST API framework), SQLAlchemy (for the ORM) and Pydantic (and schemas). The server is deployed on AWS EC2, uses AWS RDS for the Postgres SQL database and AWS Cognito for the auth. We also have an AWS Lambda for autoconfirming signed up users before the user sign up request reaches Cognito.
We used Docker and docker-compose for containerizing our application.
Frontend: React with AWS Amplify